如何执行反射调用
Java的反射调用是通过java.lang.reflect.Method的invoke调用执行,Method实例通过反射执行的方法的类的Class实例提供的方法获取,如下:
package com.test;
import java.lang.reflect.Method;
public class ReflectTest {
public void method1(int arg) {
new RuntimeException("xxxxxxxx").printStackTrace();;
}
public static void main(String[] args) throws Exception {
//获取需要反射执行的方法的Method的实例
Method method = ReflectTest.class.getMethod("method1", int.class);
//执行method的invoke方法进行反射调用,需要传入反射方法的定义的实例,以及方法的全部入参(按照参数定义的顺序)
method.invoke(new ReflectTest(), 128);
}
}
输出:
java.lang.RuntimeException: xxxxxxxx
at com.test.ReflectTest.method1(ReflectTest.java:8)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.test.ReflectTest.main(ReflectTest.java:13)
从反射调用的异常调用栈可以得知,Method实例的反射执行经过了sun.reflect.NativeMethodAccessorImpl的调用,但这只是通用的情况,下面会具体分析基于Method反射执行的实现原理。
Method反射调用的原理
查阅 Method.invoke(jdk1.8.0) 的源代码,
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
Method.invoke实际上委派给 MethodAccessor来处理。MethodAccessor 是一个接口,它有两个已有的具体实现:一个通过native code来实现反射调用,另一个则使用了java本身。在第一次调用一个实际Java方法对应得Method对象的invoke()方法之前,实现调用逻辑的MethodAccessor对象还没创建;等第一次调用时才新创建MethodAccessor并更新给root,然后调用MethodAccessor.invoke()真正完成反射调用。
委派的MethodAccessor类创建过程关键代码如下:
if (noInflation) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
如果noInflation标志位为false,将会创建DelegatingMethodAccessorImpl实现类,DelegatingMethodAccessorImpl又是代理了NativeMethodAccessorImpl(native code实现的MethodAccessor)。DelegatingMethodAccessorImpl:
class DelegatingMethodAccessorImpl extends MethodAccessorImpl {
private MethodAccessorImpl delegate;
DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
setDelegate(delegate);
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}
void setDelegate(MethodAccessorImpl delegate) {
this.delegate = delegate;
}
}
为了权衡两个版本的性能,Sun的JDK使用了“inflation”的技巧:让Java方法在被反射调用时,开头若干次使用native版,等反射调用次数超过阈值时则生成一个专用的MethodAccessor实现类,生成其中的invoke()方法的字节码,以后对该Java方法的反射调用就会使用Java版。
设置切换MethodAccessor的实现类的阀值参数为:-Dsun.reflect.inflationThreshold 。反射调用的 Inflation 机制是可以通过参数(-Dsun.reflect.noInflation=true)来关闭的。这样一来,在反射调用一开始便会直接生成Java实现的MethodAccessor。
Java版本的MethodAccessor实现大致如下(基于开头的com.test.ReflectTest类版本):
package sun.reflect;
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public GeneratedMethodAccessor1() {
super();
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException {
// prepare the target and parameters
if (obj == null) throw new NullPointerException();
try {
com.test.ReflectTest target = (com.test.ReflectTest) obj;
if (args.length != 1) throw new IllegalArgumentException();
int arg0 = (int) args[0];
} catch (ClassCastException e) {
throw new IllegalArgumentException(e.toString());
} catch (NullPointerException e) {
throw new IllegalArgumentException(e.toString());
}
// make the invocation
try {
target.method1(arg0);
} catch (Throwable t) {
throw new InvocationTargetException(t);
}
}
}
可见Java版本的MethodAccessor实现,其反射调用可以认为是对目标方法的invokevirtual调用。当该反射调用成为热点时,它甚至可以被内联到靠近Method.invoke()的一侧,大大降低了反射调用的开销。而native版的反射调用则无法被有效内联,因而调用开销无法随程序的运行而降低。
两种实现的性能比较
- Java实现的版本在初始化时需要较多时间,但是执行性能较好
- native版本正好相反,启动时相对较快
- native版本将会阻碍JVM的优化(跨越native边界会对优化有阻碍作用,它就像个黑箱一样让虚拟机难以分析也将其内联)
性能优化
Java版本的MethodAccessor实现的反射调用的性能之所以得到优化,主要是因为即时编译器中的方法内联。在关闭了 Inflation 的情况下,内联的瓶颈在于 Method.invoke 方法中对 MethodAccessor.invoke 方法的调用。由于 Java 虚拟机的关于上述调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录下调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况。
下面比较因为调用点的类型profile超出阀值导致JVM无法内联优化热点反射调用的性能损耗,先是看看能够内联优化的情况:
public static void main(String[] args) throws Exception {
Method method = ReflectTest.class.getMethod("method1", int.class);
// polluteProfile();
ReflectTest obj = new ReflectTest();
long cur = System.currentTimeMillis();
for(int i=0;i<2000000000;i++) {
if(i % 100000000 == 0) {
long tmp = System.currentTimeMillis();
System.out.println("cost=" + (tmp - cur));
cur = tmp;
}
method.invoke(obj, 128);
}
最后五次耗时统计输出:
cost=980
cost=1150
cost=1132
cost=1341
cost=1417
以下是通过创建被反射的类多个方法的Method对象并存的场景,扰乱JVM的热点内联优化:
//扰乱内联优化的方法
private static void polluteProfile() throws Exception {
Method method1 = ReflectTest.class.getMethod("method2", int.class);
Method method2 = ReflectTest.class.getMethod("method3", int.class);
ReflectTest obj = new ReflectTest();
for(int i=0;i<2000;i++) {
method1.invoke(obj, 1);
method1.invoke(obj, 2);
}
}
public static void main(String[] args) throws Exception {
Method method = ReflectTest.class.getMethod("method1", int.class);
polluteProfile();
ReflectTest obj = new ReflectTest();
long cur = System.currentTimeMillis();
for(int i=0;i<2000000000;i++) {
if(i % 100000000 == 0) {
long tmp = System.currentTimeMillis();
System.out.println("cost=" + (tmp - cur));
cur = tmp;
}
method.invoke(obj, 128);
}
}
最后五次耗时统计输出:
cost=5545
cost=5521
cost=5488
cost=5536
cost=5488
可以提高 Java 虚拟机关于每个调用能够记录的类型数目(对应虚拟机参数 -XX:TypeProfileWidth,默认值为 2,实测-XX:TypeProfileWidth=3作为JVM参数在本地笔记本无效)。