JVM的反射调用实现

如何执行反射调用

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参数在本地笔记本无效)。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容