Java 反射调用的实现

1,260 阅读6分钟

     本文将以 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 方法的调用流程。

getMethod.png

看到 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

image.png

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);
    }

image.png

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

在完成切换后,会将 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.noInflationsun.reflect.inflationThreshold 来控制关闭或调整触发阈值。
  • native 反射调用实现中,是由 C++ 代码去操作运行时栈帧,准备模板方法的数据环境,并使用 call entry_point 完成调用目标方法。

参考资料

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

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