https://time.geekbang.org/column/article/12192
反射的应用情景
- 在日常开发中使用Java编译器(IDE)的时候,当敲入点号时,IDE会根据点号之前的内容动态展示可以访问的字段与方法(在文章后续的评论中有人指出IDE在实现这个功能大部分是使用语法树来实现的)
- 在开发过程中使用的框架,例如Spring的IOC便是依赖反射机制
反射调用的实现
public final class Method extends Executable {
...
public Object invoke(Object obj, Object... args) throws ... {
... // 权限检查
MethodAccessor ma = methodAccessor;
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
}
当查看Method.invoke源码时可以发现它的具体实现时委派给MethodAccessor来处理的,MethodAccessor是一个接口它有三个具体实现类:
-
NativeMethodAccessorImpl:通过本地实现来实现反射调用
-
DelegatingMethodAccessorImpl:应用了委派模式,反射具体实现都先通过这个委派然后进行下一步调用,例如调用本地实现
-
MethodAccessorImpl:实际上只是这个类实现了MethodAccessor接口,上面两个类都是继承这个类
委派模式的原理为类B和类A是两个互相没有任何关系的类,B具有和A一模一样的方法和属性,并且调用B中的方法、属性就是调用A中同名的方法和属性。B好像就是一个受A授权委托的中介。第三方的代码不需要知道A的存在,也不需要和A发生直接的联系,通过B就可以直接使用A的功能,这样既能够使用到A的各种功能,又能够很好的将A保护起来,一举两得。
- 每个Method实例的第一次调用都会生成一个委派实现,它所委派的是一个本地实现. 当进入Java虚拟机内部时,我们拥有了Method实例所指向的方法的具体地址,只要将准备好的参数传入.
具体调用栈:反射调用---->Method.invoke---->委派实现(DelegatingMethodAccessorImpl)---->本地实现(NativeMethodAccessorImpl)---->目标方法
- 当反射调用超过15次时,Java会使用动态生成字节码的实现方式直接使用invoke指令调用目标方法. 这种方式比本地实现要快20倍,因为动态实现无需经过Java到C++再到Java的切换,但是因为生成字节码的消耗,仅仅一次调用的话反而是本地实现要快3倍左右
上面15这个是Java设置的阀值,可以通过-Dsun.reflect.inflationThreshold= 来调整.
反射调用的 Inflation 机制是可以通过参数(-Dsun.reflect.noInflation=true)来关闭的。这样一来,在反射调用一开始便会直接生成动态实现,而不会使用委派实现或者本地实现。
Inflation 指将委派时间的委派对象切换到动态实现的过程
反射调用的开销
在反射调用之前我们进行Class.forName,Class.getMethod,然后才是Method.invoke. 其中Class.forName调用了本地方法,Class.getMethod会遍历这个类的所有公有方法,如果没找到目标会查找父类的所有公有方法,这两个方法将十分耗时;同时在以getMethod为代表的查找方法会返回一份结果的拷贝,因此在热点方法中应该避免这类方法,减少堆空间消耗
接下来是反射自身的性能开销的影响因素:
- 当反射调用位于热点代码中,同样会触发即时编译,即时编译会将反射目标方法的调用内联进来,从而消除反射的消耗
- Method.invoke是一个可变长参数方法,在字节码中,它的最后一个参数是Object参数,Java编译器会在方法调用处生成一个长度为传入参数数量的Object数组,并且将传入参数一一存入这个Object数组中
- 因为Object数组不能存基本类型,所以Java编译器会对基本数据类型自动装箱