介绍

每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。

类加载相当于 Class 对象的加载。类在第一次使用时才动态加载到 JVM 中,可以使用 Class.forName(“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。

反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。

Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

  • Field

  • Method

  • Constructor

Java反射机制是指在运行时动态地获取一个类的信息并能够操作该类的属性和方法的能力。Java反射机制使得程序能够在运行时借助Class类的API来操作自身的属性和方法,从而大大增强了Java的灵活性和可扩展性。

优缺点

优点:

  1. 提高了程序的灵活性:可以在运行时获取和操作类的信息,使程序具有更好的灵活性和扩展性。

  2. 减少了代码的重复性:可以动态地获取和操作类的信息,减少了代码的重复性。

  3. 提高了程序的可维护性:可以使程序的结构更加清晰明了,提高了程序的可维护性。

缺点:

  1. 性能较低:Java反射机制是通过运行时动态获取和操作类的信息,性能较低。

  2. 安全性问题:Java反射机制可以访问和操作类的所有信息,存在安全性问题。

如何获取

比较四种获取方式的区别?通过类加载器获取的方式不常用,在此不做比较。

  1. 类名.class:JVM将使用类装载器,将类装入内存(前提是:类还没有装入内存),不做类的初始化工作,返回Class的对象。

  2. Class.forName(“类名字符串”):装入类,并做类的静态初始化,返回Class的对象。

  3. 实例对象.getClass():对类进行静态初始化、非静态初始化;返回引用运行时真正所指的对象(子对象的引用会赋给父对象的引用变量中)所属的类的Class的对象。

应用场景

  1. 框架设计:在框架设计中,通常需要使用反射技术来解耦,使框架可扩展和灵活。

  2. 单元测试:在单元测试中,我们可以使用反射技术来访问私有或受保护的类成员,使测试更加全面。

  3. 动态代理:使用反射技术可以创建动态代理对象,从而可以在运行时期代理任意一个实现了接口的对象,实现AOP等功能。

  4. 序列化和反序列化:许多Java序列化和反序列化工具都是基于Java反射机制实现的,例如Java的ObjectInputStream和ObjectOutputStream。

Java反射技术可以在很多场景中应用,尤其是在框架设计和组件化开发中,反射技术可以提高代码的灵活性和可扩展性,减少代码耦合性,简化代码的编写。但是,反射机制也增加了程序的复杂度,因此必须谨慎使用。

执行流程

反射获取类实例

获取类信息

首先调用了 java.lang.Class 的静态方法,获取类信息。

forName()反射获取类信息,并没有将实现留给了java,而是交给了jvm去加载。

主要是先获取 ClassLoader, 然后调用 native 方法,获取信息,加载类则是回调 java.lang.ClassLoader.

最后,jvm又会回调 ClassLoader 进类加载。

newInstance() 的实现

下面来看一下 newInstance() 的实现方式。

newInstance() 主要做了三件事:

  1. 权限检测,如果不通过直接抛出异常;

  2. 查找无参构造器,并将其缓存起来;

  3. 调用具体方法的无参构造方法,生成实例并返回;

获取构造器

下面是获取构造器的过程:

getConstructor0() 为获取匹配的构造方器;分三步:

  1. 先获取所有的constructors,然后通过进行参数类型比较;

  2. 找到匹配后,通过 ReflectionFactory copy一份constructor返回;

  3. 否则抛出 NoSuchMethodException;

如上,privateGetDeclaredConstructors(), 获取所有的构造器主要步骤;

  1. 先尝试从缓存中获取;

  2. 如果缓存没有,则从jvm中重新获取,并存入缓存,缓存使用软引用进行保存,保证内存可用;

另外,使用 relactionData() 进行缓存保存;ReflectionData 的数据结构如下。

其中,还有一个点,就是如何比较构造是否是要查找构造器,其实就是比较类型完成相等就完了,有一个不相等则返回false。

通过上面,获取到 Constructor 了。

接下来就只需调用其相应构造器的 newInstance(),即返回实例了。

返回构造器的实例后,可以根据外部进行进行类型转换,从而使用接口或方法进行调用实例功能了。

获取类实例两种方式的区别

  • Class.newInstance():只能够调用无参的构造函数,即默认的构造函数;并且要求被调用的构造函数是可见的,也即必须是public类型的;

  • Constructor.newInstance():可以根据传入的参数,调用任意构造构造函数;可以调用私有的构造函数。

在JDK9之后 class.newInstance()方法就被弃用了,详情可以看这篇文章 弃用class-newinstance

反射获取方法

获取 Method

忽略第一个检查权限,剩下就只有两个动作了。

  • 获取所有方法列表;

  • 根据方法名称和方法列表,选出符合要求的方法;

  • 如果没有找到相应方法,抛出异常,否则返回对应方法;

所以,先看一下怎样获取类声明的所有方法

很相似,和获取所有构造器的方法很相似,都是先从缓存中获取方法,如果没有,则从jvm中获取。

不同的是,方法列表需要进行过滤 Reflection.filterMethods;当然后面看来,这个方法我们一般不会派上用场。

根据方法名和参数类型过滤指定方法返回

大概意思看得明白,就是匹配到方法名,然后参数类型匹配,才可以。

  • 但是可以看到,匹配到一个方法,并没有退出for循环,而是继续进行匹配。

  • 这里是匹配最精确的子类进行返回(最优匹配)

  • 最后,还是通过 ReflectionFactory, copy 方法后返回。

调用 method.invoke() 方法

invoke时,是通过 MethodAccessor 进行调用的,而 MethodAccessor 是个接口,在第一次时调用 acquireMethodAccessor() 进行新创建。

两个Accessor详情:

进行 ma.invoke(obj, args); 调用时,调用 DelegatingMethodAccessorImpl.invoke();

最后被委托到 NativeMethodAccessorImpl.invoke(), 即:

其中, generateMethod() 是生成具体类的方法:

generate() 戳详情。

主要看这一句:ClassDefiner.defineClass(xx, declaringClass.getClassLoader()).newInstance();

在ClassDefiner.defineClass方法实现中,每被调用一次都会生成一个DelegatingClassLoader类加载器对象,这里每次都生成新的类加载器,是为了性能考虑,在某些情况下可以卸载这些生成的类,因为类的卸载是只有在类加载器可以被回收的情况下才会被回收的,如果用了原来的类加载器,那可能导致这些新创建的类一直无法被卸载。

而反射生成的类,有时候可能用了就可以卸载了,所以使用其独立的类加载器,从而使得更容易控制反射类的生命周期。

反射调用流程小结

  1. 反射类及反射方法的获取,都是通过从列表中搜寻查找匹配的方法,所以查找性能会随类的大小方法多少而变化;

  2. 每个类都会有一个与之对应的Class实例,从而每个类都可以获取method反射方法,并作用到其他实例身上;

  3. 反射也是考虑了线程安全的,放心使用;

  4. 反射使用软引用relectionData缓存class信息,避免每次重新从jvm获取带来的开销;

  5. 反射调用多次生成新代理Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;

  6. 当找到需要的方法,都会copy一份出来,而不是使用原来的实例,从而保证数据隔离;

  7. 调度反射方法,最终是由jvm执行invoke0()执行;

反射为什么慢

为了放大问题,找到共性,采用逐渐扩大测试次数、每次测试多次取平均值的方式,针对同一个方法分别就直接调用该方法、反射调用该方法、直接调用该方法对应的实例、反射调用该方法对应的实例分别从 1-1000000,每隔一个数量级测试一次:

测试代码如下:

测试结果如下: 测试结论:

反射的确会导致性能问题。反射导致的性能问题是否严重跟使用的次数有关系,如果控制在100次以内,基本上没什么差别,如果调用次数超过了100次,性能差异会很明显。

四种访问方式,直接访问实例的方式效率最高;其次是直接调用方法的方式,耗时约为直接调用实例的 1.4 倍;接着是通过反射访问实例的方式,耗时约为直接访问实例的 3.75 倍;最慢的是通过反射访问方法的方式,耗时约为直接访问实例的 6.2 倍。

到底慢在哪?

跟踪源码可以发现,四个方法中都存在实例化ProgramMonkey的代码,所以可以排除是这句话导致的不同调用方式产生的性能差异;通过反射调用方法中调用了setAccessible方法,但该方法纯粹只是设置属性值,不会产生明显的性能差异;所以最有可能产生性能差异的只有getMethod和getDeclaredField、invokeset方法了,下面分别就这两组方法进行测试,找到具体慢在哪?

首先测试invokeset方法,修改getReflectCallMethodCostTimegetReflectCallFieldCostTime方法的代码如下:

沿用上面的测试方法,测试结果如下: 修改getReflectCallMethodCostTimegetReflectCallFieldCostTime方法的代码如下,对getMethodgetDeclaredField进行测试:

沿用上面的测试方法,测试结果如下: 测试结论:

  • getMethodgetDeclaredField方法会比invokeset方法耗时;

  • 随着测试数量级越大,性能差异的比例越趋于稳定;

由于测试的这四个方法最终调用的都是native方法,无法进一步跟踪。个人猜测应该是和在程序运行时操作class有关。

比如需要判断是否安全?是否允许这样操作?入参是否正确?是否能够在虚拟机中找到需要反射的类?主要是这一系列判断条件导致了反射耗时;也有可能是因为调用natvie方法,需要使用JNI接口,导致了性能问题(参照Log.java、System.out.println,都是调用native方法,重复调用多次耗时很明显)。

如何避免反射导致的性能问题?

通过上面的测试可以看出,过多地使用反射,的确会存在性能问题,但如果使用得当,所谓反射导致性能问题也就不是问题了,关于反射对性能的影响,参照下面的使用原则,并不会有什么明显的问题:

  • 不要过于频繁地使用反射,大量地使用反射会带来性能问题;

  • 通过反射直接访问实例会比访问方法快很多,所以应该优先采用访问实例的方式。

小结

上面的测试并不全面,但在一定程度上能够反映出反射的确会导致性能问题,也能够大概知道是哪个地方导致的问题。如果后面有必要进一步测试,可以从下面几个方面作进一步测试:

  • 测试频繁调用native方法是否会有明显的性能问题;

  • 测试同一个方法内,过多的条件判断是否会有明显的性能问题;

  • 测试类的复杂程度是否会对反射的性能有明显影响。