Using Method.invoke as an entry point, this article will learn about the two ways in which reflection implements calls in the JDK, and analyze switching conditions and switching modes.

The reflection API

As you are all experienced drivers, I will not talk too much about the use of the reflection API.

Class<? > clazz = Class.forName("git.frank.load.User");
Object o = clazz.newInstance();

Method foo = clazz.getMethod("foo", String.class);
foo.invoke(o,"bar");
Copy the code

Root & methodAccessor reuse

Before we start, let’s do an experiment.

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));
Copy the code

The output is false and true.

As you can see, multiple calls to getMethod with the same parameter do not return the same object.

Get Method instance

Take a look at the flow of calling the getMethod method.

You can see why copyMethod is not returned multiple times.

Method Object hierarchy

Java.lang.reflect. Method has an important member attribute: 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;
Copy the code

The root object is designed to share the MethodAccessors object.

Also, each Java method has only one method object as root, and a copy of root is returned to the user each time the method object is retrieved via getMethod. At the same time, the copied object is associated with root.

By delegating hierarchically, you not only protect the cached Method objects from external arbitrary changes, but also effectively use the generated MethodAccessors.

CopyMethod logic is as follows:

  • Create a new Method object based on existing properties.
  • Setting the root pointer
  • Set the methodAccessor pointer
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;
Copy the code

Get methodAccessor

The acquireMethodAccessor method is used uniformly when obtaining methodAccessor.

In this method, the logic obtained from root is encapsulated to achieve hierarchical reuse of 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;
Copy the code

It is important to note that this method is not modified with synchronization and is stated in the comments:

// 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.
Copy the code

Multiple MethodAccessors can be created for the same Method due to concurrency issues.

Invoke delegate implementation

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);
}
Copy the code

As you can see, the Invoke logic in reflection is primarily left to MethodAccessor.

And the method to get MethodAccessor, acquireMethodAccessor, which is what we saw above, uniformly takes the MethodAccessor object associated with root.

Create MethodAccessor

As you can see, MethodAccessor is an interface, and the implementation class to use is again ReflectionFactory#newMethodAccessor.

The main logic is this 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;
}
Copy the code

Here, there are two associated configurations:

    // "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;
Copy the code

As noted in the comments, MethodAccessor comes in two implementations, a Java version that uses a Bytecodes implementation and a native version.

The Java version is 3-4 times slower than Native due to the code generation required for the first use, but subsequent calls are 20+ times faster.

Unfortunately, this can greatly affect the startup time of your application. To avoid this effect, the JVM uses the Native version for the first few reflection calls, then switches to a Bytecode-based reflection implementation.

Reflection implementation switch

Designed in order to complete the switch: DelegatingMethodAccessorImpl.

    DelegatingMethodAccessorImpl(MethodAccessorImpl delegate) {
        setDelegate(delegate);
    }

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

Can see that in the initial stage, the pointer DelegatingMethodAccessorImpl and NativeMethodAccessorImpl holding each other.

After complete the switch will DelegatingMethodAccessorImpl switch to generate GeneratedMethodAccessor.

Dynamically generated class implementation

With the default configuration, a new methodAccessor implementation is generated and loaded by the JDK on the 16th reflection call. Let’s take a look at the class loading by adding -verbose:class and looping reflection.

As you can see, the JVM loads this class before the 16th reflection call:

[Loaded sun.reflect.GeneratedMethodAccessor1 from __JVM_DefineClass__]
Copy the code

This class is dynamically generated by the JDK using MethodAccessorGenerator.

Since this is in the form of concatenated bytecode, which is hardly readable, let’s just look at the result:

public class GeneratedMethodAccessor1 extends MethodAccessorImpl {
    public GeneratedMethodAccessor1(a) {}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 newInvocationTargetException(var3); }}}}Copy the code

As you can see, the reflection call after the GeneratedMethodAccessor is a normal invokevirtual call to the target method.

Native call implementation

Corresponds to NativeMethodAccessorImpl.

Its related methods are modified by native keywords:

    private static native Object invoke0(Method m, Object obj, Object[] args);
Copy the code

Moving on to the openJDK source code, see how Java methods are called in native code:

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);
}
Copy the code

The key is the Invoke method, which does a lot of data validation and preparation work, and I won’t go into detail here.

Fast forward directly to 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(a);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)
Copy the code

As you can also guess from the method naming, Java methods called in C++ don’t actually execute real Java code, but instead go into a pile method.

What is a stub method?

The stub code is like the Agent code generated at the service consumer side in RPC calls. It does the remote communication for you, encapsulates parameters, and so on, and makes the user feel as if using local methods.

What is a Stub code?

Then look at the call_stub, where the actual call is _call_stub_entry.

  static CallStub call_stub(a)           { return CAST_TO_FN_PTR(CallStub, _call_stub_entry); }
Copy the code

And _call_stub_entry is a function pointer:

    StubRoutines::_call_stub_entry =
      generate_call_stub(StubRoutines::_call_stub_return_address);
Copy the code

Finally, we come to the generate_call_stub method, where we are ready to execute the run data needed for the specified code and jump to execution.

Create call stack frames

  • First, the register state needs to be saved before calling:
    const Address saved_rbx     (rbp, - 3 * wordSize);
    const Address saved_rsi     (rbp, 2 - * wordSize);
    const Address saved_rdi     (rbp, - 1 * wordSize);
Copy the code
  • Then, the parameters needed to call the target method are pushed:
    // stub code
    __ enter(a);
    __ 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
Copy the code
  • In addition, since Java method call parameters are passed in reverse order, we need to reverse the stack parameters:
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);
Copy the code

After the stack frame is created, the stack frame should look like this in the comment:

  //---------------------------------------------------------------------------------------------------------------------- --
  // 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 ]
Copy the code

jump

  • Finally, use the saved method entry to be called:entry_point, the use ofcallComplete the jump and execute the function 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);
Copy the code

Get the return value

  • When the method call is complete, save the return value type and result.
    // 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);
Copy the code

Restore the stack frame

  • Clear the parameters that were previously pushed.
    // pop parameters
    __ lea(rsp, rsp_after_call);
Copy the code
  • Restore register:
    // restore rdi, rsi and rbx,
    __ movptr(rbx, saved_rbx);
    __ movptr(rsi, saved_rsi);
    __ movptr(rdi, saved_rdi);
    __ addptr(rsp, 4*wordSize);
Copy the code
  • Add return statements.
    // return
    __ pop(rbp);
    __ ret(0);
Copy the code

At this point, one method call is complete.

conclusion

  • Getting the same method multiple timesmethodObjects do not get the same object instance, but they all have the same root object.
  • In Java, reflection calls passmethodA self-maintained two-layer tree structure is assigned to the same onemethodAccessorThe implementation.
  • Under the default configuration, the first 15 reflection calls are implemented in native mode, and the 16th reflection calls are dynamically generated in the form of concatenated bytecode to optimize the subsequent reflection calls asinvokevirtual.
  • Because dynamic bytecode generation is time-consuming, it is not triggered directly at the beginning, and can be passedsun.reflect.noInflationsun.reflect.inflationThresholdTo control off or adjust trigger thresholds.
  • Native reflection call implementation, is by C++ code to manipulate the runtime stack frame, prepare the template method data environment, and usecall entry_pointFinish calling the target method.

The resources

A log about the reflection call method

Geek Time: Taking Apart the Java Virtual Machine in depth