JEP draft: Reimplement Core Reflection on Method Handles

OwnerMandy Chung
TypeFeature
ScopeJDK
StatusSubmitted
Componentcore-libs / java.lang:reflect
EffortM
DurationM
Reviewed byAlan Bateman, John Rose
Endorsed byJohn Rose
Created2021/04/26 22:41
Updated2021/06/15 17:43
Issue8266010

Summary

Reimplement the underlying implementation used by java.lang.reflect.Method Constructor, and Field to be built on top of java.lang.invoke method handles. This makes method handles be the underlying mechanism for the reflection support for the platform and reduces the maintenance and development overhead for java.lang.reflect and java.lang.invoke APIs.

Goals

Non-Goals

Motivation

Core reflection has two implementations for invoking methods and constructors. For fast startup, core reflection uses the VM native reflection implementation for the first few invocations. After a number of invocations, bytecode is generated for the reflective method for use in subsequent invocations. Combined, these two implementations avoid the class loading overhead for less frequently-called methods. For field access, the implementation uses the internal Unsafe API for fast access.

With java.lang.invoke method handle API introduced since Java 7, there are altogether three different implementations of reflective mechanisms.

Because of this, when java.lang.reflect and java.lang.invoke are required to be updated to support new language features for example Project Valhalla, the cost of modifying all three different code paths is high. In addition, the current implementation relies on VM special rules on these generated reflection classes, all subclasses of MagicAccessorImpl, including (see details in JDK-xxxxx):

This JEP proposes to reimplement java.lang.reflect on top of method handles as the common underlying reflective mechanism for the platform. It will reduce the development cost in upgrading the reflection support for new language features such as primitive classes and generic specialization for Project Valhalla. Furthermore, the VM hacks that the current implementation relies on can be removed.

The VM native reflection implementation is still needed for early VM startup before method handles are ready to be used. This JEP proposes to limit the use of VM native reflection implementation and use method handles as early as possible. This will benefit from Project Loom as there is no native frame due to core reflection on the stack.

Description

Replace the implementation of Method::invoke, Constructor::newInstance, Field::get and Field::set with direct invocation of the method handle of the reflected member.

The microbenchmarks show that the performance of the new implementation of Method::invoke, Field::get and Field::set on instance fields is faster than the old implementation. Field::set on static fields and Constructor::newInstance have a small performance regression which will be further investigated for improvement. The performance of Field::get on static fields is comparable to the old implementation. The cold startup time of a simple application doing Method::invoke on 32 methods has increased from 64ms running with the old implementation to 70ms running with the new implementation. Cold startup performance improvement will continue to be explored.

Method Handles

The best performance of the method handle invocation in the current HotSpot VM implementation is when MethodHandle is loaded from a constant. The new implementation uses a similar approach as the old implementation to maintain an invocation counter that for the first few invocations of a method it will invoke the corresponding MethodHandle directly. When it reaches a threshold, it will spin dynamic bytecode stub that is defined as a hidden class and the target MethodHandle is loaded from its class data as a dynamically computed constant for better performance.

VM native reflection implementation is needed for early VM startup before the method handle mechanism is fully initialized. Once the method handle mechanism is fully initialized (that happens soon after System::initPhase1 and before System::initPhase2), core reflection will switch to use method handles.

Caller-sensitive methods

A caller-sensitive method can behave differently depending on the identity of its immediate caller. The implementation of a caller-sensitive method does a stack walk to find its immediate caller skipping stack frames representing the internal reflection machinery.

Here are some examples of caller-sensitive methods in the platform.

This example code shows how a caller-sensitive method CSM::returnCallerClass is called via Method::invoke.

class CSM {
   @CallerSensitive static Class<?> returnCallerClass() {
        return Reflection.getCallerClass();
    }
}

class Foo {
    void test() throws Throwable {
        // calling CSM::returnCallerClass via reflection
        var m =  CSM.class.getMethod("returnCallerClass");
        // expect Foo is the caller class
        var caller = m.invoke(null);
        assert(caller == Foo.class);
    }
}

First, Method::invoke finds Foo as its immediate caller to check. It checks if CSM::returnCallerClass is accessible to Foo. Then it invokes CSM::returnCallerClass reflectively. As CSM::returnCallerClass is a caller-sensitive method, it also finds its immediate caller class (with all reflection frames skipped) and returns it. In this case, CSM::returnCallerClass finds Foo as the caller class. The stack trace would look something like this:

CSM.returnCallerClass
    jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
    jdk.internal.reflect.NativeMethodAccessorImpl.invoke
    jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
    java.lang.reflect.Method.invoke
    Foo.test
    :
    :

Note that the stack walking is done twice to find the caller class, one for Method::invoke and one for CSM::returnCallerClass.

Method handle invocation for caller-sensitive methods

If a method handle for a caller-sensitive method is requested, the general rules for bytecode behaviors apply, but they take account of the lookup class in a special way. The resulting method handle behaves as if it were called from an instruction contained in the lookup class, so that the caller-sensitive method detects the lookup class. (By contrast, the invoker of the method handle is disregarded.) Thus, in the case of caller-sensitive methods, different lookup classes may give rise to differently behaving method handles.

Due to this behavior of caller-sensitive method, an invocation on a target caller-sensitive method via Method::invoke, which is called via a method handle does not work properly. For example, Bar calls CSM::returnCallerClass via a chained reflective call as shown below:

class Bar {
    void test() throws Throwable {
        // method handle for Method::invoke
        MethodHandle mh = MethodHandles.lookup().findVirtual(Method.class, "invoke",
                                                             methodType(Object.class, Object.class, Object[].class));
        // reflective object for CSM::returnCallerClass
        Method m =  CSM.class.getMethod("returnCallerClass");
        // invoke Method::invoke via method handle and the target method
        // being invoked reflectively is CSM::returnCallerClass
        var caller = mh.invoke(m, null, null);
        assert(caller == Bar.class);           // Fail!
    }
}

It is reasonable to expect that this chained reflective call to invoke CSM::returnCallerClass should behave the same as CSM::returnCallerClass is called statically, i.e. Bar should be the return class but the current implementation returns a wrong caller class.

The stack trace below shows the internal implementation (the hidden frames are included) that reveals what is found as the caller class through stack walking. On the other hand, Method::invoke is called via a method handle. Method::invoke should behave as if it were called by the lookup class of the Lookup object creating that method handle as specified, i.e. Bar.

CSM.returnCallerClass()
    jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
    jdk.internal.reflect.NativeMethodAccessorImpl.invoke
    jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
    java.lang.reflect.Method.invoke(mh)
    java.lang.invoke.DirectMethodHandle$Holder.invokeSpecial
    java.lang.invoke.LambdaForm$MH/0x0000000800003000.invoke
    java.lang.invoke.LambdaForm$MH/0x0000000800004400.invokeExact_MT
    Bar$$InjectedInvoker/0x0000000800003400.invoke_V             <--- caller
    java.lang.invoke.DirectMethodHandle$Holder.invokeStatic
    java.lang.invoke.LambdaForm$MH/0x0000000800004000.invoke
    java.lang.invoke.LambdaForm$MH/0x0000000800003c00.invoke_MT
    Bar.test
    :
    :

This example shows a bug in the current implementation that two caller-sensitive methods does not work properly if called a chained reflective call if relying on stack walking to find the caller.

The current implementation injects a hidden class Bar$$InjectedInvoker/0x0000000800003400 which is in the same runtime package as Bar and defined by the same defining loader as Bar with the same protection domain for security reason. Stack walking will find Bar$$InjectedInvoker/0x0000000800003400 as the caller class instead of Bar. This approach works for most caller-sensitive methods which depend on the runtime package, the defining loader or protection domain of the real caller class but does not work for MethodHandles::lookup which requires the exact caller class (see JDK-8013527 and JDK-8257874 for details).

Special calling sequence for caller-sensitive methods

The new implementation introduces a special calling sequence for caller-sensitive methods. A caller-sensitive method can provide a private adapter of the same name but with an additional Class parameter along side. When a caller-sensitive method is invoked via core reflection or via a method handle, it will find if an adapter method with a Class parameter is present. If found, it will invoke the adapter method with the caller class argument instead. This special calling sequence ensures that the same caller class is passed to a caller-sensitive method via Method::invoke or MethodHandle::invokeExact or a mix of these methods.

For example, CSM::returnCallerClass and its adapter method will look like this:

class CSM {
    @CallerSensitive static Class<?> returnCallerClass() {
        return returnCallerClass(Reflection.getCallerClass());
    }

    private static Class<?> returnCallerClass(Class<?> caller) {
        return caller;
    }
}

The stack trace for the examples above would look like this in the new implementation:

CSM.returnCallerClass(caller)                               <--- adapter method
    java.lang.invoke.DirectMethodHandle$Holder.invokeStatic
    java.lang.invoke.Invokers$Holder.invokeExact_MT
    jdk.internal.reflect.DirectMethodAccessorImpl$CallerSensitiveWithCaller.invoke
    java.lang.reflect.Method.invoke
    Foo.test
   :
   :

and

CSM.returnCallerClass(caller)                                 <--- adapter method
    java.lang.invoke.DirectMethodHandle$Holder.invokeStatic
    java.lang.invoke.Invokers$Holder.invokeExact_MT
    jdk.internal.reflect.DirectMethodAccessorImpl$CallerSensitiveWithCaller.invoke
    java.lang.reflect.Method.invoke(caller, m)              <--- adapter method
    java.lang.invoke.DirectMethodHandle$Holder.invokeSpecial
    java.lang.invoke.LambdaForm$MH/0x0000000800004000.invoke
    java.lang.invoke.LambdaForm$MH/0x0000000800003c00.invoke_MT
    Bar.test
    :
    :

Both CSM::returnCallerClass and Method::invoke can have an adapter method with a caller class parameter defined. Foo calls Method::invoke which does the stack walking to find the caller class. It will pass the caller class directly to the adapter method for CSM::returnCallerClass.

Similarly, Bar calls Method::invoke via method handle to invoke CSM::returnCallerClass. In that case, MethodHandle::invokeExact uses the lookup class of the Lookup object producing the method handle as the caller class and so no stack walking involved. The lookup class is Bar. It will invoke the adapter method of Method::invoke with Bar as the caller class, that in turn invokes the adapter method of CSM::returnCallerClass with Bar as the caller. The new implementation eliminates the need of multiple stack walking when a caller-sensitive method is invoked reflectively.

For caller-sensitive methods that require an exact caller class, the adapter method must be defined for correctness. MethodHandles::lookup and ClassLoader::registerAsParallelCapable are the only two methods in the JDK that needs the exact caller class.

On the other hand, for caller-sensitive methods that uses the caller class for access check or security permission check, i.e. based on its runtime package, defining loader, or protection domain, the adapter method is optional.

The new implementation will support reflective calls on caller-sensitive methods with and without an adapter using the special calling sequence.

Alternatives

Alternative 1: No change to existing implementation

Keep existing core reflection implementation to avoid any compatibility risk. The dynamic bytecode generated for core reflection remains at class file version 49 and all the existing hacks in the VM will stay to workaround the reflection issues, for example, VM continues to skip verification of these reflection implementation classes.

For projects like Valhalla, the cost of updating java.lang.reflect and java.lang.invoke to support primitive classes and generic specialization is high. New VM hacks may be needed to support new language features with the limitation of old classfile format.

Project Loom will need to look for a different solution to address the limitation of native frames due to core reflection.

Alternative 2: Upgrade to a new bytecode library

Replace the bytecode writer used by core reflection to use a new bytecode library that evolves with the new class file format. This alternative makes it possible for core reflection to use new classfile format but yet it still requires to update the different code paths for java.lang.reflect and java.lang.invoke to support new language features.

This alternative has a lower compatibility risk than the proposed JEP. However, it is still a sizeable amount of work and requires comprehensive testing. The existing VM hacks to workaround the reflection issues remain to stay.

Similarly, Project Loom will need to look for a different solution to address the limitation of native frames due to core reflection.

Testing

Comprehensive JDK testing is required to ensure that the implementation is robust and compatible with existing behavior.
Performance testing is required to ensure no significant performance regression compared to the current implementation. Developers using the Early Access build will be encouraged to test as many libraries and frameworks as possible to help us identify any behavior or performance regressions.

The performance result of the microbenchmarks shows no significant performance regression and also some good improvement in many benchmarks.

As a reference, one snapshot of the performance result is shown below and we continue to explore other performance improvement opportunities.

Baseline

Benchmark                                           Mode  Cnt    Score    Error  Units
ReflectionFields.getInt_instance_field              avgt   10    8.058 ±  0.003  ns/op
ReflectionFields.getInt_instance_field_var          avgt   10    7.576 ±  0.097  ns/op
ReflectionFields.getInt_static_field                avgt   10    5.937 ±  0.002  ns/ops
ReflectionFields.getInt_static_field_var            avgt   10    6.810 ±  0.027  ns/ops
ReflectionFields.setInt_instance_field              avgt   10    5.102 ±  0.023  ns/ops
ReflectionFields.setInt_instance_field_var          avgt   10    5.139 ±  0.006  ns/ops
ReflectionFields.setInt_static_field                avgt   10    4.245 ±  0.002  ns/ops
ReflectionFields.setInt_static_field_var            avgt   10    3.920 ±  0.003  ns/ops
ReflectionMethods.class_forName_1arg                avgt   10  407.448 ±  0.823  ns/ops
ReflectionMethods.class_forName_1arg_var            avgt   10  418.611 ±  8.790  ns/ops
ReflectionMethods.class_forName_3arg                avgt   10  366.685 ±  5.713  ns/ops
ReflectionMethods.class_forName_3arg_var            avgt   10  359.410 ±  3.926  ns/ops
ReflectionMethods.instance_method                   avgt   10   17.428 ±  0.020  ns/ops
ReflectionMethods.instance_method_var               avgt   10   20.249 ±  0.065  ns/ops
ReflectionMethods.static_method                     avgt   10   18.843 ±  0.035  ns/ops
ReflectionMethods.static_method_var                 avgt   10   19.460 ±  0.050  ns/ops

New implementation

Benchmark                                           Mode  Cnt     Score     Error  Units
ReflectionFields.getInt_instance_field              avgt   10     6.361 ±   0.002  ns/op
ReflectionFields.getInt_instance_field_var          avgt   10     5.976 ±   0.112  ns/op
ReflectionFields.getInt_static_field                avgt   10     5.946 ±   0.003  ns/op
ReflectionFields.getInt_static_field_var            avgt   10     6.372 ±   0.014  ns/op
ReflectionFields.setInt_instance_field              avgt   10     4.672 ±   0.013  ns/op
ReflectionFields.setInt_instance_field_var          avgt   10     3.933 ±   0.009  ns/op
ReflectionFields.setInt_static_field                avgt   10     4.661 ±   0.001  ns/op
ReflectionFields.setInt_static_field_var            avgt   10     3.953 ±   0.014  ns/op
ReflectionMethods.class_forName_1arg                avgt   10   404.300 ±   1.423  ns/op
ReflectionMethods.class_forName_1arg_var            avgt   10   402.458 ±   0.418  ns/op
ReflectionMethods.class_forName_3arg                avgt   10   394.287 ±   3.443  ns/op
ReflectionMethods.class_forName_3arg_var            avgt   10   377.586 ±   0.270  ns/op
ReflectionMethods.instance_method                   avgt   10    13.645 ±   0.019  ns/op
ReflectionMethods.instance_method_var               avgt   10    13.811 ±   0.029  ns/op
ReflectionMethods.static_method                     avgt   10    13.723 ±   0.026  ns/op
ReflectionMethods.static_method_var                 avgt   10    13.164 ±   0.046  ns/op

Risks and Assumptions

Frameworks or libraries that depend on highly implementation-specific and undocumented aspects of the existing code generation implementation may have a compatibility impact. To mitigate the compatibility risk, it will be possible to run the applications with -Djdk.reflect.useDirectMethodHandle=false using the old implementation as a workaround until it is fixed in a JDK release.