美文网首页
Java反射实现原理

Java反射实现原理

作者: 花醉霜寒 | 来源:发表于2019-07-30 19:25 被阅读0次

\color{ForestGreen}{反射实现}
Java反射应用十分广泛,例如spring的核心功能控制反转IOC就是通过反射来实现的,本文主要研究一下发射方法调用的实现方式和反射对性能的影响。
如下为Method类中invoke方法,可以看出该方法实际是将反射方法的调用委派给MethodAccessor,通过MethodAccessor的invoke方法完成方法调用。该接口有两个现有的实现类,一个是直接调用本地方法,另一个是通过委派模式。Mehod的实例第一次调用都会生成一个委派实现,它实现的就是一个本地实现,相当于在Method和本地实现之间加了一个中间层,本地实现比较好理解,就是讲参数准备好,然后进入目标方法。通过如下代码进行一个简单的测试,通过打印的堆栈信息可以看出,Method实例的invoke方法首先调用的是委派实现DelegatingMethodAccessorImpl的invoke方法,最后调用本地实现。那么为什么会加上一个看似多余的中间委派层呢?实际上,java语言实现反射的方法除了本地实现之外,还会采用动态生成字节码的方式直接调用目标方法,运用委派模式的目的就是在本地实现和动态实现之间进行切换。

public class TestReflect {
    public static void target(int i) {
        new RuntimeException("让我们来看看方法调用栈").printStackTrace();
    }
    public static void main(String[] args) throws Exception {
    Class<?> klass = Class.forName("com.kafka.demo.com.kafka.demo.test.TestReflect");
        Method method = klass.getMethod("target", int.class);
        method.invoke(null, 128);
    }
}

方法调用栈如下所示

java.lang.RuntimeException: 让我们来看看方法调用栈
    at com.kafka.demo.com.kafka.demo.test.TestReflect.target(TestReflect.java:12)
    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.kafka.demo.com.kafka.demo.test.TestReflect.main(TestReflect.java:47)

动态实现和本地实现相比,其运行效率是本地实现的20倍左右,但是第一次生成字节码是比较耗时的,所以仅单次调用的话,本地实现的性能反而是动态实现的3到4倍。JVM通过参数-Dsun.reflect.inflationThresHold来进行调整,默认是15次,即前15次调用会使用本地实现,15次之后会生成字节码,采用动态实现的方式。我们将方法的调用循环20次得到结果如下所示:

java.lang.RuntimeException: 让我们来看看第15次方法调用栈
    at com.kafka.demo.com.kafka.demo.test.TestReflect.target(TestReflect.java:12)
    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.kafka.demo.com.kafka.demo.test.TestReflect.main(TestReflect.java:48)
java.lang.RuntimeException: 让我们来看看第16次方法调用栈
    at com.kafka.demo.com.kafka.demo.test.TestReflect.target(TestReflect.java:12)
    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.kafka.demo.com.kafka.demo.test.TestReflect.main(TestReflect.java:48)
java.lang.RuntimeException: 让我们来看看第17次方法调用栈
    at com.kafka.demo.com.kafka.demo.test.TestReflect.target(TestReflect.java:12)
    at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.kafka.demo.com.kafka.demo.test.TestReflect.main(TestReflect.java:48)

在第十六次方法调用的过程中动态生成了字节码,并在第十七次方法调用的过程中就直接使用GenerateMethodAccessor1了。

\color{ForestGreen}{反射性能}
一般都认为反射是比较消耗性能的,就连甲骨文关于反射的教学中也强调反射性能开销大的缺点。从上面的例子来看,Class.forName()需要调用本地方法,getMethod()方法需要遍历目标类的所有方法,如果没有匹配到还需要遍历父类的所有方法,这些都是比较消耗性能的操作,但是我们可以将它们的实例对象放在缓存中,来减小对性能的影响,这里我们主要讨论Method实例的invoke方法对性能的影响。我做了一个简单的实验来进行验证,将一个空方法直接调用20亿次,每1亿次打印一个该阶段的运行时间,取最后五次时间求取平均值(这样做的目的是为了得到预热峰值性能),在我32G内存的ThinkStation上面直接调用1亿次耗时90ms,通过反射调用平均耗时为300ms,约为直接调用的3.3倍。前文中介绍过动态实现和本地实现,由于本地实现比较消耗性能,通过-Dsun.reflect.noInflation=true来关闭本地实现直接使用动态实现,此时平均耗时为270ms,约为直接调用的3倍。此外方法调用时需要检查方法的权限,如果跳过权限检测过程,即设置method.setAccessible(true),此时平均耗时为210ms,约为直接调用的2倍左右,至此我们可以看出对于反射的应用会对性能造成一定的影响,但是可以通过优化减小影响。那是不是为了性能就杜绝使用反射呢?显然不是,前文中提到IOC就是用反射实现的,类似接口和实现类,接口的使用也是对性能有影响的(虽然这个影响可以忽略不计,也许这个类比也不太确切),但是不能为了一点性能的提升而放弃优雅的设计模式。

public class TestReflect {
    public static void target(int i) {
        // 空方法
    }
    public static void main(String[] args) throws Exception {
        //-Dsun.reflect.noInflation=true
        Class<?> klass = Class.forName("com.kafka.demo.com.kafka.demo.test..TestReflect");
        Method method = klass.getMethod("target", int.class);
        //method.setAccessible(true);
        LocalDateTime t1 = LocalDateTime.now();
        for (int i = 1; i <= 2_000_000_000; i++) {
            if (i % 100_000_000 == 0) {
                long temp = System.currentTimeMillis();
                System.out.println(Duration.between(t1, LocalDateTime.now()).toMillis());
                t1 = LocalDateTime.now();
            }
            //method.invoke(null, 128);
            target(128);
        }
    }
}

相关文章

网友评论

      本文标题:Java反射实现原理

      本文链接:https://www.haomeiwen.com/subject/bytvlctx.html