Java 反射调用的实现 反射 API root & met

本文将以 `method.invoke` 为入口,学习 JDK 中反射实现调用的两种方式,并分析切换条件和切换方式。

反射 API

大家都是老司机了,关于反射 API 的使用不再过多介绍。

1
2
3
4
5
java复制代码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 复用

开始之前,我们先来做一个实验。

1
2
3
4
5
java复制代码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 方法的调用流程。

getMethod.png

看到 copyMethod ,就知道为什么多次返回的 method 对象不是同一个了吧~。

method 对象层级

java.lang.reflect.Method 中有一个很重要的成员属性:root

1
2
3
4
5
6
7
java复制代码// 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 指针
1
2
3
4
5
6
7
java复制代码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 的复用。

1
2
3
4
5
6
7
8
9
10
11
java复制代码    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 修饰,在注释中也有说明:

1
2
3
4
java复制代码// 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

image.png

invoke 委派实现

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码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;
}

这里,会有两个关联的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    // "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

1
2
3
4
5
6
7
8
9
java复制代码    DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
setDelegate(delegate);
}

public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
return delegate.invoke(obj, args);
}

image.png

可以看到到,在初始阶段,DelegatingMethodAccessorImplNativeMethodAccessorImpl 互相持有对方的指针。

在完成切换后,会将 DelegatingMethodAccessorImpl 切换至生成的 GeneratedMethodAccessor

动态生成类实现

在默认配置下,在第 16 次反射调用时,JDK 会生成一个新的 methodAccessor 实现并加载。
我们先通过添加 -verbose:class,并循环调用反射看一下类加载情况。

可以看到,在第 16 次反射调用前,JVM 加载了这个类:

1
java复制代码[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]

而这个类是 JDK 使用 MethodAccessorGenerator 动态生成出来的。

由于这里采用的是拼接字节码的形式,几乎没有什么可读性,我们就看一下他生成的结果就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码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 关键字修饰的:

1
java复制代码    private static native Object invoke0(Method m, Object obj, Object[] args);

接下来跟进到 openJDK 源码中,看一下在本地代码中是如何调用 java 方法的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c++复制代码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 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C++复制代码  // 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

1
C++复制代码  static CallStub call_stub()           { return CAST_TO_FN_PTR(CallStub, _call_stub_entry); }

_call_stub_entry 是一个函数指针:

1
2
C++复制代码    StubRoutines::_call_stub_entry =
generate_call_stub(StubRoutines::_call_stub_return_address);

最后终于来到了 generate_call_stub 方法中,在这里准备好执行指定代码所需的运行数据,并跳转执行。

建立调用栈帧

  • 首先,在调用前需要先对寄存器状态进行保存:
1
2
3
C++复制代码    const Address saved_rbx     (rbp, -3 * wordSize);
const Address saved_rsi (rbp, -2 * wordSize);
const Address saved_rdi (rbp, -1 * wordSize);
  • 然后,对调用目标方法需要的参数进行压栈:
1
2
3
4
5
6
7
C++复制代码    // 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 中方法调用参数是逆序传递的,需要再将栈中参数顺序翻转:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C++复制代码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);

当其建立完栈帧后,栈帧应该是注释里面写的这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
C++复制代码  //------------------------------------------------------------------------------------------------------------------------
// 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 完成跳转,执行函数调用。
1
2
3
4
5
6
C++复制代码    __ 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);

获取返回值

  • 当方法调用完成后,保存返回值类型和结果。
1
2
3
4
5
6
7
8
9
10
11
C++复制代码    // 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);

恢复栈帧

  • 清除之前压栈放入的参数。
1
2
C++复制代码    // pop parameters
__ lea(rsp, rsp_after_call);
  • 恢复寄存器:
1
2
3
4
5
C++复制代码    // restore rdi, rsi and rbx,
__ movptr(rbx, saved_rbx);
__ movptr(rsi, saved_rsi);
__ movptr(rdi, saved_rdi);
__ addptr(rsp, 4*wordSize);
  • 添加返回语句。
1
2
3
C++复制代码    // return
__ pop(rbp);
__ ret(0);

至此,就完成了一次方法调用。

总结

  • 多次获取相同方法的 method 对象得到的并不是同一个对象实例,但是他们都有共同的根对象。
  • java 中反射调用会通过 method 自身维护的一个二层树型结构统一委派给同一个 methodAccessor 实现。
  • 在默认配置下,前 15 次反射调用会使用 native 的方式实现,在第 16 次反射调用时,会采用拼接字节码的形式动态生成调用点,将后续的反射调用优化为 invokevirtual
  • 由于动态生成字节码比较耗时,所以并没有一开始就直接触发,可以通过 sun.reflect.noInflationsun.reflect.inflationThreshold 来控制关闭或调整触发阈值。
  • native 反射调用实现中,是由 C++ 代码去操作运行时栈帧,准备模板方法的数据环境,并使用 call entry_point 完成调用目标方法。

参考资料

关于反射调用方法的一个log

极客时间 <深入拆解Java虚拟机>

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%