在上一篇文章《细读源码之JAVA反射》一文中,我们首先讲解了反射的应用场景以及缺点,其中反射调用一个非常致命的缺点,就是运行效率低下。
为了解决这个问题,JDK高版本对其进行了优化
一.反射优化举例
Java反射调用,有严重的性能问题,JDK高版本对其进行了优化。本文以JDK1.8为例,我们讲解一下反射调用优化的过程:
通过动态生成class字节码文件,把反射调用过程中的native调用,替换为纯Java方法调用,来提高性能。
由于替换的过程非常耗时,JDK并不是在首次反射调用就直接进行优化,而是通过ReflectionFactory.noInflation和ReflectionFactory.inflationThreshold两个参数来控制替换的时机。
下面举个例子,说明这一过程:
1.测试类定义:
run方法里面仅执行throw Exception的操作,作用是为了打印出调用的堆栈信息,方便后面进行分析。
2.测试程序:
执行的时候,需要加上运行参数-Dsun.reflect.inflationThreshold=0,加这个参数的原因,在后面会详细解答。上面代码执行结果如下所示:
分析上面的日志,可以得出下面结论:
1.第一次反射调用,是通过代理类DelegatingMethodAccessorImpl去调用NativeMethodAccessorImpl.invoke0这个方法,去触发run方法的执行,invoke0是native调用;
2.在第一次反射调用的时候,该方法反射调用的次数,大于inflationThreshold设置的值0,开始进行反射优化,将DelegatingMethodAccessorImpl的代理对象被替换为动态生成的GeneratedMethodAccessor2;
3.第二次反射调用,调用GeneratedMethodAccessor2.invoke方法,该方法里再去调用run方法,此invoke方法是纯Java调用。
综上,我们可以得出上面的结论:反射调用的过程中,会把耗时的native调用,替换为纯Java调用,这样JVM可以对字节码进行优化,来提高反射执行的效率。
二.反射优化时机控制参数
Java反射优化,通过ReflectionFactory.noInflation和ReflectionFactory.inflationThreshold这两个参数来控制反射优化的时机。
noInflation参数控制是否在第一次反射调用的时候中就进行优化;noInflation设置为true表示首次反射调用就进行优化,inflationThreshold的值就没有用了;noInflation设置为false表示首次反射调用,不进行优化,待反射方法执行的次数,大于inflationThreshold的设置值后,再进行优化。
有一些场景,想要禁用反射优化,我们只需要把noInflation设置为false,inflationThreshold设置为Integer.MAX_VALUE即可,就可以达到禁用的效果。
1.参数定义
noInflation和inflationThreshold的定义在ReflectionFactory类中,代码如下:
启动Java程序,如果不增加额外的启动参数,noInflation的默认值为false,inflationThreshold的默认值为15。
2.参数初始化
分析上面代码,我们可以通过-Dsun.reflect.noInflation,来设置noInflation的值,通过-Dsun.reflect.inflationThreshold来设置inflationThreshold的值。
合理的设置这两个值,可以提高反射的执行效率。对于高频反射执行的方法(设计上应该尽量避免这种发生情况),可以设置noInflation=true,然后通过自动化程序,触发反射执行,达到预热效果,然后再去承载线上正常的流量。
如果没有预热过程,首次调用时间会很长,影响用户体验。
3.举例
还是第一部分的代码,如果我们替换运行参数为-Dsun.reflect.noInflation=true,再次运行,会得到下面的结果:
运行结果,和参数为-Dsun.reflect.inflationThreshold=0是的结果完全不同。
noInflation=true的时候,首次调用的时候就直接优化成纯java调用,没有了上面的替换的过程。
三.核心方法解析
Java反射的一次调用过程,如下图所示:
下面对流程图的中的各个方法,做详细说明:
1.Methed.invoke方法
invoke方法是反射调用的入口,就从这个方法开始分析,代码如下:
从上面代码可以看出,Methed.invoke最终执行的是MethodAccessor.invoke方法,MethodAccessor是接口,里面只定义了一个invoke方法。
此接口一共有3个实现类MethodAccessorImpl,DelegatingMethodAccessorImpl,NativeMethodAccessorImpl,下面我们看一下这3个类的实现。
2.MethodAccessorImpl
MethodAccessorImpl是抽象类,内部使用到MethodAccessor接口的地方,都替换成了MethodAccessorImpl,并把可见性设置为包可见,目的是为了收窄权限。
3.DelegatingMethodAccessorImpl
DelegatingMethodAccessorImpl从命名上就可以知道,该类使用了代理模式,是代理类。
真正执行操作的是delegate.invoke方法,delegate是被代理对象,它也继承了MethodAccessorImpl类。
运行时,可以通过调用setDelegate去替换被代理对象。
反射优化前delegate设置是NativeMethodAccessorImpl实例,优化后delegate就被替换成了动态生成的纯Java调用的版本。
4.NativeMethodAccessorImpl
NativeMethodAccessorImpl是反射native调用的版本,numInvocations字段是计数器,记录反射已经被调用的次数,当++this.numInvocations > ReflectionFactory.inflationThreshold时,启用优化机制,调用MethodAccessorGenerator.generateMethod生成一个动态类,调用DelegatingMethodAccessorImpl.setDelegate进行实现方式替换。
5.GeneratedMethodAccessor
GeneratedMethodAccessor类是运行时调用MethodAccessorGenerator.generateMethod动态生成的,用来替换NativeMethodAccessorImpl实现,通过反编译查看,GeneratedMethodAccessor类定义如下:
GeneratedMethodAccessor2继承了MethodAccessorImpl并实现了invoke方法,invoke实现非常简单,直接对传入的Object进行强制类型转换为目标类型,然后显示地执行其方法,在此示例中,是直接调用的run方法,实现了纯java调用。
6.MethodAccessorGenerator.generateMethod
generateMethod方法非常长,摘取部分代码如下:
其中this.asm.emitMagicAndVersion()代码如下:
又看到了-889275714这个熟悉的数字,十六进制表示是cafebabe,Java Class文件的魔数,上次看到这个数字是讲动态代理类生成的时候遇到的。
反射优化的动态类也是按照class file的标准,直接操作二进制数据生成的,然后调用ClassDefiner.defineClass进行类加载,最后通过newInstance的方法创建其一个实例。
7.ReflectionFactory.acquireMethodAccessor
在入口方法Methed.invoke中MethodAccessor对象,是通过调用acquireMethodAccessor来获取的,代码如下:
要想读懂这个方法,首先要了解Method对象的基本构成。每个Java方法有且只有一个Method对象作为root,它相当于根对象,对用户不可见。
当我们调用getMethod等方法来获取Method对象时,我们获得的Method对象都是root对象的副本。
root对象持有一个MethodAccessor对象,所有获取到的Method对象都共享这一个MethodAccessor对象,所以开始的时候会执行root.getMethodAccessor来获取。
如果获取的对象为null,表示该方法还没有被反射调用过,调用newMethodAccessor进行methodAccessor初始化。
8.ReflectionFactory.newMethodAccessor
newMethodAccessor中使用了noInflation配置,当设置为true时,首次反射调用就进行优化,动态生成纯Java版本的调用。
设置为false的时候,则使用native版本。
到此,Java反射优化的内容就讲完了