反射的实现
- 横向比对C#,几乎是是一样的
核心是invoke函数
1 | public final class Method extends Executable { |
调用过程
- 如果打印stack trace可以看到西面的代码,从中可以看到java的反射本质是依次调用
DelegatingMethodAccessorImpl
、NativeMethodAccessorImpl
,最后调用对象方法。 - 这个思路说白了就是在C++层面(native开头的API)查询到对象函数的地址,然后调用。
1 | import java.lang.reflect.Method; |
- 当多次调用(这个次数由
Dsun.reflect.inflationThreshold
决定)后,JVM会动态构建一个函数的字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。
1 | java.lang.Exception: #16 |
反射的开销
外部消耗
- 从上面的实现可以看出,反射需要调用forName、getMethod等方法,这些方法调用本身就会产生消耗。Class.forName 会调用本地方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。
- 以 getMethod 为代表的查找方法操作,会返回查找得到结果的一份拷贝。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。
内部消耗
在invoke函数中需要传递一个object数组,这个是会产生内存消耗的。
同时,基础类型转object有装箱消耗。不过java会对[-128,127]之间的数字对应的Integer对象有cache。
还有java会做逃逸分析,也就是说下面代码中invoke函数中的object会被认为是不逃逸的,那么即时编译器可以选择栈分配甚至是虚拟分配,也就是不占用堆空间。
最后是内联函数的实现,由于 Java 虚拟机调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录下调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况。可以提高 Java 虚拟机关于每个调用能够记录的类型数目(对应虚拟机参数 -XX:TypeProfileWidth,默认值为 2,这里设置为 3)。
1 | Import java.lang.reflect.Method; |