本文将以 method.invoke
为入口,学习 JDK 中反射实现调用的两种方式,并分析切换条件和切换方式。
反射 API
大家都是老司机了,关于反射 API 的使用不再过多介绍。
Class<?> clazz = Class.forName("git.frank.load.User");
Object o = clazz.newInstance();
Method foo = clazz.getMethod("foo", String.class);
foo.invoke(o,"bar");
root & methodAccessor 复用
开始之前,我们先来做一个实验。
Method foo1 = clazz.getMethod("foo", String.class);
Method foo2 = clazz.getMethod("foo", String.class);
System.out.println(foo1 == foo2);
System.out.println(foo1.equals(foo2));
输出分别是 false true
。
可以看出,相同参数多次调用 getMethod
返回的并不是同一个对象。
获取 Method 实例
先看一下 getMethod
方法的调用流程。
看到 copyMethod
,就知道为什么多次返回的 method 对象不是同一个了吧~。
method 对象层级
java.lang.reflect.Method
中有一个很重要的成员属性:root
。
// For sharing of MethodAccessors. This branching structure is
// currently only two levels deep (i.e., one root Method and
// potentially many Method objects pointing to it.)
//
// If this branching structure would ever contain cycles, deadlocks can
// occur in annotation code.
private Method root;
该 root
对象就是为了共享 MethodAccessors
对象而设计的。
并且,每个 java 方法只会有一个 method
对象做为 root ,在每次通过 getMethod
获取 method
对象时,都会把 root
复制一份返回给用户。同时,会把复制出的对象与 root
建立关联。
通过这种层级委派的方式,不仅保护了缓存的 method
对象不会被外部随意更改,又可以有效使用已生成的 MethodAccessors
。
copyMethod
逻辑如下:
- 根据现有属性创建出新的 method 对象。
- 设置 root 指针
- 设置 methodAccessor 指针
Method res = new Method(clazz, name, parameterTypes, returnType,
exceptionTypes, modifiers, slot, signature,
annotations, parameterAnnotations, annotationDefault);
res.root = this;
// Might as well eagerly propagate this if already present
res.methodAccessor = methodAccessor;
return res;
获取 methodAccessor
在获取 methodAccessor
时,统一使用 acquireMethodAccessor
方法。
在该方法内,封装了到 root
中获取的逻辑,完成基于层级结构对 methodAccessor
的复用。
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
值得注意的是,该方法并没有被 synchronization
修饰,在注释中也有说明:
// NOTE that there is no synchronization used here. It is correct
// (though not efficient) to generate more than one MethodAccessor
// for a given Method. However, avoiding synchronization will
// probably make the implementation more scalable.
可能会由于并发问题造成同一个 Method
被创建出多个 MethodAccessor
。
invoke 委派实现
java.lang.reflect.Method#invoke
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
...
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
可以看出,反射中的 invoke
逻辑主要是交给 MethodAccessor
去做的。
而获取 MethodAccessor
的方法,acquireMethodAccessor
就是我们上面看过的,会统一取 root 中关联的 MethodAccessor
对象。
创建 MethodAccessor
可以看到,MethodAccessor
是一个接口,具体使用的实现类还是要看 ReflectionFactory#newMethodAccessor
。
主要的判断逻辑是这个 if
。
if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
这里,会有两个关联的配置:
// "Inflation" mechanism. Loading bytecodes to implement
// Method.invoke() and Constructor.newInstance() currently costs
// 3-4x more than an invocation via native code for the first
// invocation (though subsequent invocations have been benchmarked
// to be over 20x faster). Unfortunately this cost increases
// startup time for certain applications that use reflection
// intensively (but only once per class) to bootstrap themselves.
// To avoid this penalty we reuse the existing JVM entry points
// for the first few invocations of Methods and Constructors and
// then switch to the bytecode-based implementations.
private static boolean noInflation = false;
private static int inflationThreshold = 15;
如注释所说,MethodAccessor
有两种实现,一种为使用 bytecodes 实现的 java 版本,另外一种为 native 版本。
java 版本因为在第一次使用时需要生成代码,首次调用会比 native 慢 3 - 4 倍,但后续的调用会比 native 快 20+ 倍。
不幸的是,这些会大大影响应用的启动耗时。为了避免这种影响,JVM 在前几次反射调用使用 native 版本,后续会切换为基于 bytecode 的反射实现。
反射实现切换
为了完成切换而设计的中间层 :DelegatingMethodAccessorImpl
。
DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
setDelegate(delegate);
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}
可以看到到,在初始阶段,DelegatingMethodAccessorImpl
和 NativeMethodAccessorImpl
互相持有对方的指针。
在完成切换后,会将 DelegatingMethodAccessorImpl
切换至生成的 GeneratedMethodAccessor
。
动态生成类实现
在默认配置下,在第 16 次反射调用时,JDK 会生成一个新的 methodAccessor 实现并加载。
我们先通过添加 -verbose:class
,并循环调用反射看一下类加载情况。
可以看到,在第 16 次反射调用前,JVM 加载了这个类:
[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
而这个类是 JDK 使用 MethodAccessorGenerator
动态生成出来的。
由于这里采用的是拼接字节码的形式,几乎没有什么可读性,我们就看一下他生成的结果就好了:
public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
public GeneratedMethodAccessor1() {
}
public Object invoke(Object var1, Object[] var2) throws InvocationTargetException {
if (var1 == null) {
throw new NullPointerException();
} else {
User var10000;
String var10001;
try {
var10000 = (User)var1;
if (var2.length != 1) {
throw new IllegalArgumentException();
}
var10001 = (String)var2[0];
} catch (NullPointerException | ClassCastException var4) {
throw new IllegalArgumentException(var4.toString());
}
try {
return var10000.foo(var10001);
} catch (Throwable var3) {
throw new InvocationTargetException(var3);
}
}
}
}
可以看到,在触发 GeneratedMethodAccessor
之后的反射调用就是正常的对目标方法进行 invokevirtual
调用。
native 调用实现
对应于 NativeMethodAccessorImpl
。
其相关的方法为 native
关键字修饰的:
private static native Object invoke0(Method m, Object obj, Object[] args);
接下来跟进到 openJDK 源码中,看一下在本地代码中是如何调用 java 方法的:
oop Reflection::invoke_method(oop method_mirror, Handle receiver, objArrayHandle args, TRAPS) {
oop mirror = java_lang_reflect_Method::clazz(method_mirror);
int slot = java_lang_reflect_Method::slot(method_mirror);
bool override = java_lang_reflect_Method::override(method_mirror) != 0;
objArrayHandle ptypes(THREAD, objArrayOop(java_lang_reflect_Method::parameter_types(method_mirror)));
oop return_type_mirror = java_lang_reflect_Method::return_type(method_mirror);
BasicType rtype;
if (java_lang_Class::is_primitive(return_type_mirror)) {
rtype = basic_type_mirror_to_basic_type(return_type_mirror, CHECK_NULL);
} else {
rtype = T_OBJECT;
}
InstanceKlass* klass = InstanceKlass::cast(java_lang_Class::as_Klass(mirror));
Method* m = klass->method_with_idnum(slot);
if (m == NULL) {
THROW_MSG_0(vmSymbols::java_lang_InternalError(), "invoke");
}
methodHandle method(THREAD, m);
return invoke(klass, method, receiver, override, ptypes, rtype, args, true, THREAD);
}
关键在于 invoke
方法中,里面做了大量的数据校验和准备的工作,这里也不再详细看了。
直接快进到 JavaCalls::call_helper
中:
// do call
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
StubRoutines::call_stub()(
(address)&link,
// (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
result_val_address, // see NOTE above (compiler problem)
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
result = link.result(); // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
// Preserve oop return value across possible gc points
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)
跟进方法命名也可以猜到,在 C++ 中调用的 java 方法实际上执行的并不是真正的 java 代码,而是走到了一个桩方法中。
什么是桩方法 (stub)
桩代码就像 RPC 调用中在服务消费端生成的 agent 代码,他帮你做远程通讯,封装参数等工作,使用户认为就像使用本地方法一样。
详细可以参考 R大的回答:什么是桩代码(Stub)?
接着再看 call_stub
,这里真正调用的是 _call_stub_entry
。
static CallStub call_stub() { return CAST_TO_FN_PTR(CallStub, _call_stub_entry); }
而 _call_stub_entry
是一个函数指针:
StubRoutines::_call_stub_entry =
generate_call_stub(StubRoutines::_call_stub_return_address);
最后终于来到了 generate_call_stub
方法中,在这里准备好执行指定代码所需的运行数据,并跳转执行。
建立调用栈帧
- 首先,在调用前需要先对寄存器状态进行保存:
const Address saved_rbx (rbp, -3 * wordSize);
const Address saved_rsi (rbp, -2 * wordSize);
const Address saved_rdi (rbp, -1 * wordSize);
- 然后,对调用目标方法需要的参数进行压栈:
// stub code
__ enter();
__ movptr(rcx, parameter_size); // parameter counter
__ shlptr(rcx, Interpreter::logStackElementSize); // convert parameter count to bytes
__ addptr(rcx, locals_count_in_bytes); // reserve space for register saves
__ subptr(rsp, rcx);
__ andptr(rsp, -(StackAlignmentInBytes)); // Align stack
- 另外,由于 java 中方法调用参数是逆序传递的,需要再将栈中参数顺序翻转:
Label loop;
// Copy Java parameters in reverse order (receiver last)
// Note that the argument order is inverted in the process
// source is rdx[rcx: N-1..0]
// dest is rsp[rbx: 0..N-1]
__ movptr(rdx, parameters); // parameter pointer
__ xorptr(rbx, rbx);
__ BIND(loop);
// get parameter
__ movptr(rax, Address(rdx, rcx, Interpreter::stackElementScale(), -wordSize));
__ movptr(Address(rsp, rbx, Interpreter::stackElementScale(),
Interpreter::expr_offset_in_bytes(0)), rax); // store parameter
__ increment(rbx);
__ decrement(rcx);
__ jcc(Assembler::notZero, loop);
当其建立完栈帧后,栈帧应该是注释里面写的这个样子:
//------------------------------------------------------------------------------------------------------------------------
// Call stubs are used to call Java from C
//
// [ return_from_Java ] <--- rsp
// [ argument word n ]
// ...
// -N [ argument word 1 ]
// -7 [ Possible padding for stack alignment ]
// -6 [ Possible padding for stack alignment ]
// -5 [ Possible padding for stack alignment ]
// -4 [ mxcsr save ] <--- rsp_after_call
// -3 [ saved rbx, ]
// -2 [ saved rsi ]
// -1 [ saved rdi ]
// 0 [ saved rbp, ] <--- rbp,
// 1 [ return address ]
// 2 [ ptr. to call wrapper ]
// 3 [ result ]
// 4 [ result_type ]
// 5 [ method ]
// 6 [ entry_point ]
// 7 [ parameters ]
// 8 [ parameter_size ]
// 9 [ thread ]
跳转
- 最后,使用保存的待调用的方法入口:
entry_point
,使用call
完成跳转,执行函数调用。
__ BIND(parameters_done);
__ movptr(rbx, method); // get Method*
__ movptr(rax, entry_point); // get entry_point
__ mov(rsi, rsp); // set sender sp
BLOCK_COMMENT("call Java function");
__ call(rax);
获取返回值
- 当方法调用完成后,保存返回值类型和结果。
// store result depending on type
// (everything that is not T_LONG, T_FLOAT or T_DOUBLE is treated as T_INT)
__ movptr(rdi, result);
Label is_long, is_float, is_double, exit;
__ movl(rsi, result_type);
__ cmpl(rsi, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(rsi, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(rsi, T_DOUBLE);
__ jcc(Assembler::equal, is_double);
恢复栈帧
- 清除之前压栈放入的参数。
// pop parameters
__ lea(rsp, rsp_after_call);
- 恢复寄存器:
// restore rdi, rsi and rbx,
__ movptr(rbx, saved_rbx);
__ movptr(rsi, saved_rsi);
__ movptr(rdi, saved_rdi);
__ addptr(rsp, 4*wordSize);
- 添加返回语句。
// return
__ pop(rbp);
__ ret(0);
至此,就完成了一次方法调用。
总结
- 多次获取相同方法的
method
对象得到的并不是同一个对象实例,但是他们都有共同的根对象。 - java 中反射调用会通过
method
自身维护的一个二层树型结构统一委派给同一个methodAccessor
实现。 - 在默认配置下,前 15 次反射调用会使用 native 的方式实现,在第 16 次反射调用时,会采用拼接字节码的形式动态生成调用点,将后续的反射调用优化为
invokevirtual
。 - 由于动态生成字节码比较耗时,所以并没有一开始就直接触发,可以通过
sun.reflect.noInflation
和sun.reflect.inflationThreshold
来控制关闭或调整触发阈值。 - native 反射调用实现中,是由 C++ 代码去操作运行时栈帧,准备模板方法的数据环境,并使用
call entry_point
完成调用目标方法。