概述
赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
本质其实就是根据 符号 拿到符号表以及直接地址 进行调用。
反射调用的实现原理
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 接口提供了两种实现,一种是本地方法实现,另一种是 Java 实现
那么如何拿到执行地址呐?有两种方式:
考虑到许多反射调用仅会执行一次,Java 虚拟机设置了一个阈值 15(可以通过 -Dsun.reflect.inflationThreshold= 来调整),当某个反射调用的调用次数在 15 之下时,采用本地实现;当达到 15 时,便开始动态生成字节码,并将委派实现的委派对象切换至动态实现,这个过程我们称之为 Inflation。
刚开始生成字节码比较耗时,但是随着 JIT 的即时编译,会越来越快
反射调用的开销
- 变长参数方法导致的 Object 数组
- 由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组(。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中
- 基本类型的自动装箱、拆箱
- 由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱
- 最重要的方法内联
- 由于 Java 虚拟机的关于上述调用点的类型 profile(注:对于 invokevirtual 或者 invokeinterface,Java 虚拟机会记录下调用者的具体类型,我们称之为类型 profile)无法同时记录这么多个类,因此可能造成所测试的反射调用没有被内联的情况
Method.invoke一直会被内联,但是它里面的 MethodAccesor.invoke 则不一定。
实际上,在C2编译之前循环代码已经运行过非常多次,也就是说MethodAccesor.invoke已经看到多次调用至target()的动态实现。在profile里会显示为有target1,有target2,但是profile不完整,即还有一大部分的调用者类型没有记录。
这时候C2会选择不inline这个MethodAccesor.invoke调用,直接做虚调用。