Java里的反射,允许正在运行的Java程序观测,甚至是修改程序的动态行为。可以通过Class对象枚举该类中的所有方法,还可以通过Method.setAccessible(java.lang.reflect包,继承自AccessibleObject)绕过Java语言的访问权限,在私有方法所在类之外的地方调用该方法。
应用:
- Java集成开发环境(IDE),根据敲入点号时,点号前的内容,动态展示可以访问的字段或者方法。(通过语法树实现)
- Java调试器,在调试过程中枚举某一对象所有字段的值。
- Web开发中可配置的通用框架。保证框架的可扩展性,借助Java的反射机制,根据配置文件来加载不同的类。(Spring框架的依赖反转IOC)。
反射API(reflect包的javadoc)
通常来说,使用反射API的第一步便是获取Class对象。在Java中常见的有三种:
- 使用静态方法Class.forName来获取。
- 调用对象的getClass()方法。
- 直接用类名+”.class”访问。对于基本类型来说,它们的包装类型(wrapper classer)拥有一个名为”TYPE”的final静态字段,指向该基本类型对应的Class对象。
Integer.Type指向int.class、对于数组类型来说,可以使用类名+”[].class”来访问,int[].class。
除此之外,Class类和java.lang.reflect包中还提供了许多返回Class对象的方法。对于数组类的Class对象,调用Class.getComponentType()方法可以获得数组元素的类型。
一旦得到了Class对象,便可以正式的使用反射功能了。较为常用的几项:
- 使用newInstance()来生成一个该类的实例。它要求该类中拥有一个无参数的构造器。
- 使用isInstance(Object)来判断一个对象是否该类的实例,语法上等同于instanceof关键字(JIT优化时会有差别)。
- 使用Array.newInstance(Class,int)来构造该类型的数组。
- 使用getFields()/getConstructors()/getMethods()来访问该类的成员。除了这三个之外,Class类还提供了许多其他方法。方法名中带Declared的不会返回父类的成员,但是会返回私有成员;而不带Declared的则相反。
获得了类成员之后,可以进一步做如下操作:
- 使用Constructor/Field/Method.setAccessible(true)来绕开Java语言的访问限制。
- 使用Constructor.newInstance(Object[])来生成该类的实例。
- 使用Field.get/set(Object)来访问字段的值。
- 使用Method.invoke(Object, Object[])来调用方法。
反射调用的实现
1 | public final class Method extends Executable { |
方法的反射调用-Method.invoke实际上委派给MethodAccessor来处理。MethodAccessor是一个接口,有两个具体实现:
- 通过本地方法来实现反射调用
- 使用委派模式
每个Method实例的第一次反射调用都会生成一个委派实现,它所委派的具体实现便是一个本地实现:当进入了JVM内部之后,便拥有了Method实例所指向方法的具体地址。此时,反射调用将传入的参数准备好,然后调用进入目标方法。
反射调用先是调用了Method.invoke,然后进入委派实现(DelegatingMethodAccessorImpl)中间层,再然后进入本地实现(NativeMethodAccessorImpl),最后到达目标方法。
Java的反射调用机制还设立了另一种动态生成字节码的实现。直接使用invoke指令来调用目标方法。之所以采用委派实现,是为了能够在本地实现以及动态实现中切换。
动态实现和本地实现相比,运行效率要快上20倍。因为动态实现无需经过Java到C++再到Java的切换(JNI,Java Native Interface),但生成字节码十分耗时,仅调用一次的话,本地实现反而要快上3到4倍。
许多反射调用仅会执行一次,JVM设置了一个阈值15(Dsun.reflect.inflationThreshold=15),当某个反射调用的调用次数在15之下时,采用本地实现(C++);当达到15时,开始动态生成字节码(动态生成一个Java类来做直接调用,加速反射调用),并将委派实现的委派对象切换至动态实现,这个过程称为inflation。
反射调用的Inflation机制是可以通过参数(-Dsun.reflect.noInflation=true)来关闭。在反射调用一开始便直接生成动态实现,而不会使用委派实现或者本地实现。
1 | sun.reflect.ReflectionFactory; |
反射调用的开销
反射调用过程中先后进行了Class.forName,Class.getMethod以及Method.invoke三个操作。其中Class.forName会调用本地方法,Class.getMethod则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法,这两个操作都非常费时。
以getMethod为代表的查找方法操作,会返回查找结果的一份拷贝。应当避免在热点代码中使用返回Method数组的getMethods或者getDeclaredMethods方法,以减少不必要的堆空间消耗。在实践中,会在应用程序中缓存Class.forName和Class.getMethod的结果。
Method.invoke是一个变长参数方法,在字节码层面它的最后一个参数会是Object数组。Java编译器会在方法调用处生成一个长度为传入参数数量的Object数组,并将传入参数一一存储进该数组中。
Object数组不能存储基本类型,Java编译器会对传入的基本类型参数进行自动装箱(Java缓存了[-128, 127]中所有整数所对应的Integer对象。当需要自动装箱的整数在这个范围之内,便返回缓存的Integer,否则需要新建一个Integer对象(参数-DJava.lang.Integr.IntegerCache.high=128,扩大缓存范围,避免新建Integer对象))。
上述两个Object数组的操作除了带来性能开销外,还可能占用堆内存,使得GC更加频繁(虚拟机参数-XX:+PrintGC)。
Method.setAccessible(true);关闭权限检查(每次反射调用都会检查目标方法的权限)。
即时编译器中的方法内联(编译器在编译一个方法时,将某个方法调用的目标方法也纳入编译范围内,并用其返回值替代原方法)。
在关闭了Inflation的情况下,内联的瓶颈在于Method.invoke方法中对MethodAccessor.invoke方法的调用。

在生产环境中,往往拥有多个不同的反射调用,对应多个多态实现GeneratedMethodAccessor+num。由于JVM关于上述调用点的类型profile(对于invokevirtual或者involeinterface,JVM会记录下调用者的具体类型,称为类型profile)无法同时记录这么多个类(虚拟机参数-XX:TypeProfileWidth,默认值是2。提高JVM关于每个调用能够记录的类型数目),因此可能造成反射调用没有被内联的情况。
Method.invoke就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得Method.invoke自身难以被内联到调用方。
Java&中的MethodHandle,在使用MethodHandle来做反射调用时,MethodHandle.invoke()的形式参数与返回值类型都是准确的,所以只需要在链接方法的时候才需要检查类型的匹配性,而不必在每次调用时都检查。而且MethodHandle是不可变值,在创建后其内部状态就不会再改变了;JVM可以利用这个知识而放心的对它做激进优化,例如将实际的调用目标内联到做反射调用的一侧。
关于反射调用方法的一个log
总结
在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。在调用超过15次之后,委派实现便会将委派对象切换至动态实现。这个动态实现的字节码是自动生成的,它将直接使用invoke指令来调用目标方法。
方法的反射调用会带来不少性能开销,主要有三个原因:变长参数方法导致的Object数组,基本类型的自动装箱、拆箱,方法内联。