本文将以 `method.invoke` 为入口,学习 JDK 中反射实现调用的两种方式,并分析切换条件和切换方式。
反射 API
大家都是老司机了,关于反射 API 的使用不再过多介绍。
1 | java复制代码Class<?> clazz = Class.forName("git.frank.load.User"); |
root & methodAccessor 复用
开始之前,我们先来做一个实验。
1 | java复制代码Method foo1 = clazz.getMethod("foo", String.class); |
输出分别是 false true
。
可以看出,相同参数多次调用 getMethod
返回的并不是同一个对象。
获取 Method 实例
先看一下 getMethod
方法的调用流程。
看到 copyMethod
,就知道为什么多次返回的 method 对象不是同一个了吧~。
method 对象层级
java.lang.reflect.Method
中有一个很重要的成员属性:root
。
1 | java复制代码// For sharing of MethodAccessors. This branching structure is |
该 root
对象就是为了共享 MethodAccessors
对象而设计的。
并且,每个 java 方法只会有一个 method
对象做为 root ,在每次通过 getMethod
获取 method
对象时,都会把 root
复制一份返回给用户。同时,会把复制出的对象与 root
建立关联。
通过这种层级委派的方式,不仅保护了缓存的 method
对象不会被外部随意更改,又可以有效使用已生成的 MethodAccessors
。
copyMethod
逻辑如下:
- 根据现有属性创建出新的 method 对象。
- 设置 root 指针
- 设置 methodAccessor 指针
1 | java复制代码Method res = new Method(clazz, name, parameterTypes, returnType, |
获取 methodAccessor
在获取 methodAccessor
时,统一使用 acquireMethodAccessor
方法。
在该方法内,封装了到 root
中获取的逻辑,完成基于层级结构对 methodAccessor
的复用。
1 | java复制代码 MethodAccessor tmp = null; |
值得注意的是,该方法并没有被 synchronization
修饰,在注释中也有说明:
1 | java复制代码// NOTE that there is no synchronization used here. It is correct |
可能会由于并发问题造成同一个 Method
被创建出多个 MethodAccessor
。
invoke 委派实现
1 | java复制代码java.lang.reflect.Method#invoke |
可以看出,反射中的 invoke
逻辑主要是交给 MethodAccessor
去做的。
而获取 MethodAccessor
的方法,acquireMethodAccessor
就是我们上面看过的,会统一取 root 中关联的 MethodAccessor
对象。
创建 MethodAccessor
可以看到,MethodAccessor
是一个接口,具体使用的实现类还是要看 ReflectionFactory#newMethodAccessor
。
主要的判断逻辑是这个 if
。
1 | java复制代码if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) { |
这里,会有两个关联的配置:
1 | java复制代码 // "Inflation" mechanism. Loading bytecodes to implement |
如注释所说,MethodAccessor
有两种实现,一种为使用 bytecodes 实现的 java 版本,另外一种为 native 版本。
java 版本因为在第一次使用时需要生成代码,首次调用会比 native 慢 3 - 4 倍,但后续的调用会比 native 快 20+ 倍。
不幸的是,这些会大大影响应用的启动耗时。为了避免这种影响,JVM 在前几次反射调用使用 native 版本,后续会切换为基于 bytecode 的反射实现。
反射实现切换
为了完成切换而设计的中间层 :DelegatingMethodAccessorImpl
。
1 | java复制代码 DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) { |
可以看到到,在初始阶段,DelegatingMethodAccessorImpl
和 NativeMethodAccessorImpl
互相持有对方的指针。
在完成切换后,会将 DelegatingMethodAccessorImpl
切换至生成的 GeneratedMethodAccessor
。
动态生成类实现
在默认配置下,在第 16 次反射调用时,JDK 会生成一个新的 methodAccessor 实现并加载。
我们先通过添加 -verbose:class
,并循环调用反射看一下类加载情况。
可以看到,在第 16 次反射调用前,JVM 加载了这个类:
1 | java复制代码[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__] |
而这个类是 JDK 使用 MethodAccessorGenerator
动态生成出来的。
由于这里采用的是拼接字节码的形式,几乎没有什么可读性,我们就看一下他生成的结果就好了:
1 | java复制代码public class GeneratedMethodAccessor1 extends MethodAccessorImpl { |
可以看到,在触发 GeneratedMethodAccessor
之后的反射调用就是正常的对目标方法进行 invokevirtual
调用。
native 调用实现
对应于 NativeMethodAccessorImpl
。
其相关的方法为 native
关键字修饰的:
1 | java复制代码 private static native Object invoke0(Method m, Object obj, Object[] args); |
接下来跟进到 openJDK 源码中,看一下在本地代码中是如何调用 java 方法的:
1 | c++复制代码oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) { |
关键在于 invoke
方法中,里面做了大量的数据校验和准备的工作,这里也不再详细看了。
直接快进到 JavaCalls::call_helper
中:
1 | C++复制代码 // do call |
跟进方法命名也可以猜到,在 C++ 中调用的 java 方法实际上执行的并不是真正的 java 代码,而是走到了一个桩方法中。
什么是桩方法 (stub)
桩代码就像 RPC 调用中在服务消费端生成的 agent 代码,他帮你做远程通讯,封装参数等工作,使用户认为就像使用本地方法一样。
详细可以参考 R大的回答:什么是桩代码(Stub)?
接着再看 call_stub
,这里真正调用的是 _call_stub_entry
。
1 | C++复制代码 static CallStub call_stub() { return CAST_TO_FN_PTR(CallStub, _call_stub_entry); } |
而 _call_stub_entry
是一个函数指针:
1 | C++复制代码 StubRoutines::_call_stub_entry = |
最后终于来到了 generate_call_stub
方法中,在这里准备好执行指定代码所需的运行数据,并跳转执行。
建立调用栈帧
- 首先,在调用前需要先对寄存器状态进行保存:
1 | C++复制代码 const Address saved_rbx (rbp, -3 * wordSize); |
- 然后,对调用目标方法需要的参数进行压栈:
1 | C++复制代码 // stub code |
- 另外,由于 java 中方法调用参数是逆序传递的,需要再将栈中参数顺序翻转:
1 | C++复制代码Label loop; |
当其建立完栈帧后,栈帧应该是注释里面写的这个样子:
1 | C++复制代码 //------------------------------------------------------------------------------------------------------------------------ |
跳转
- 最后,使用保存的待调用的方法入口:
entry_point
,使用call
完成跳转,执行函数调用。
1 | C++复制代码 __ BIND(parameters_done); |
获取返回值
- 当方法调用完成后,保存返回值类型和结果。
1 | C++复制代码 // store result depending on type |
恢复栈帧
- 清除之前压栈放入的参数。
1 | C++复制代码 // pop parameters |
- 恢复寄存器:
1 | C++复制代码 // restore rdi, rsi and rbx, |
- 添加返回语句。
1 | C++复制代码 // return |
至此,就完成了一次方法调用。
总结
- 多次获取相同方法的
method
对象得到的并不是同一个对象实例,但是他们都有共同的根对象。 - java 中反射调用会通过
method
自身维护的一个二层树型结构统一委派给同一个methodAccessor
实现。 - 在默认配置下,前 15 次反射调用会使用 native 的方式实现,在第 16 次反射调用时,会采用拼接字节码的形式动态生成调用点,将后续的反射调用优化为
invokevirtual
。 - 由于动态生成字节码比较耗时,所以并没有一开始就直接触发,可以通过
sun.reflect.noInflation
和sun.reflect.inflationThreshold
来控制关闭或调整触发阈值。 - native 反射调用实现中,是由 C++ 代码去操作运行时栈帧,准备模板方法的数据环境,并使用
call entry_point
完成调用目标方法。
参考资料
本文转载自: 掘金