在JVM中反射是怎样实现
先看示例代码:
public class Solution {
public static void show(int i) {
new Exception("#" + i).printStackTrace();
}
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("Solution");
Method method = clazz.getMethod("show", int.class);
method.invoke(null, 0);
}
}
从以上代码看 Method.invoke来执行反射方法调用,为方便查看调用哪些类,打印了show方法的栈信息:
java.lang.Exception: #0
at Solution.show(Solution.java:15)
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 Solution.main(Solution.java:21)
Method的invoke方法,看一下实现:
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);
}
}
可以看到,实际上是委派给了MethodAccessor来处理,MethodAccessor是一个接口,有2个已有的具体实现: 一个通过本地方法(NativeMethodAccessorImpl)来实现反射,即本地实现;一个则使用了委派模式(DelegatingMethodAccessorImpl),即委派实现。
在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用.在调用超过15次之后,委派实现便会将委派对象切换至动态实现,这个动态实现的字节码是自动生成的,将直接使用invoke指令来调用目标方法.
带来的性能开销
上面示例代码中,先后使用了Class.forName
,Class.getMethod
及Method.invoke
三个调用。其中Class.forName会调用本地方法,Class.getMethod会遍历该类的公有方法.如果没有匹配到,它还将遍历父类的公有方法,所以这两个操作都是非常耗时的。
以getMethod为代表的查找方法操作,会返回查找得到结果的一份拷贝.因此,应当避免在热点代码中使用返回Method数组的getMethods或者getDeclaredMethods方法,以减少不必要的堆空间消耗。
除反射调用外,还额外做了两个操作:
- Method.invoke是一个变长参数方法,在字节码层面它的最后一个参数是Object数组.编译器会在方法调用处生成一个长度为传入参数数量的Object数组,并将传入参数一一存储进该数组中.
- 由于Object数组不能存储基本类型,编译器会将传入的基本数据类型进行自动装箱.
影响反射调用耗时有以下原因:
- 方法表查找
- 构建Object数组以及可能存在的自动装拆箱操作
- 运行时权限检查
- 可能没有方法内联/逃逸分析
优化反射性能开销
从引起性能开销的原因入手:
- 尽量避免反射调用虚方法
- 关闭运行时权限检查 :
setAccessible(true)
- 可能需要增大基本数据类型对应的包装类缓存
- 关闭Inflation机制(直接动态生成字节码)
- 提高JVM关于每个调用能够记录的类型数目