JEP draft: 用方法句柄重写反射
翻译自JEP draft: Reimplement Core Reflection with Method Handles (java.net)
译者:Method handler :方法句柄
| Owner | Mandy Chung |
|---|---|
| Type | Feature |
| Scope | JDK |
| Status | Submitted |
| Component | core-libs / java.lang:reflect |
| Effort | M |
| Duration | M |
| Reviewed by | Alan Bateman, John Rose |
| Endorsed by | John Rose |
| Created | 2021/04/26 22:41 |
| Updated | 2021/07/28 23:19 |
| Issue | 8266010 |
总概
在java.lang.invoke method handlers基础上重新实现java.lang.reflect.Method, Constructor, 和Field。 使方法句柄作为底层机制的反射将降低java.lang.reflect和java.lang.invokeAPI的维护和开发成本
非目标
不会对java.lang.reflect API 进行任何更改。这仅仅是一个对其底层实现的修改。
动机
对于调用方法和构造器的反射有两种内部机制。为了快速启动,对于前几次对特定反射方法和构造器的调用会使用Hotspot虚拟机的native方法,为了更好的性能,在调用若干次之后,它会为反射操作生成字节码并在之后的调用中使用这些字节码
对于字段的访问,反射则是使用内部的sun.misc.UnsafeAPI
通过 Java 7 中引入的 java.lang.invoke 方法句柄 API,共有三种不同的内部反射操作机制:
- 虚拟机的native方法
- 为
Method::invoke和Constructor::newInstance动态生成的字节码,以及对Field::get和setUnsafe的字段访问机制 - 方法句柄
当我们为了支持新的语言特性而去更新java.lang.reflect和java.lang.invoke,例如在 Project Valhalla预计的那些新功能可能需要我们修改所有的三条代码路径,这些修改的代价将是十分高昂的。除此之外,现有的实现依赖于虚拟机对生成的字节码的特殊处理,其被包裹在jdk.internal.reflect.MagicAccessorImpl的子类之中:
译者:虚拟机对生成的字节码的特殊处理
- 放宽可访问性,以便于这些类去访问其他类的不可访问字段和方法
- 校验被关闭以解决JLS §6.6.2以支持对
Object::clone的反射 - 一个性能表现不好的类加载器将用于解决安全性和兼容性问题
描述
基于方法句柄重新实现的java.lang.reflect将作为平台通用的反射底层实现机制,以替代基于字节码生成机制的Method::invoke, Constructor::newInstance, Field::get, 和Field::set
对于特定反射对象上这些反射方法之一的前几次调用,我们直接调用相应的方法句柄。 之后我们将旋转(spun)定义在[隐藏类](docs.oracle.com/en/java/jav…. Lookup.html#defineHiddenClassWithClassData(byte[],java.lang.Object,boolean,java.lang.invoke.MethodHandles.Lookup.ClassOption...))的字节码,其将从[类数据](docs.oracle.com/en/java/jav…. String,java.lang.Class))加载目标MethodHandle 作为 动态计算常量。 从常量加载方法句柄允许 HotSpot VM 内联方法句柄调用以实现良好的性能。
在方法句柄机制初始化之前,早期启动阶段仍然需要虚拟机的native反射方法。这将发生在 System::initPhase1 之后和 System::initPhase2之前,在此之后我们将切换为只使用方法句柄。通过减少本地方法栈帧有利于Project Loom
微基准测试表明,实例成员上 Method::invoke、Field::get 和 Field::set 的新实现的性能比旧实现更快。 Field::get 在静态字段上的性能与旧实现相当。 Field::set 在静态字段和 Constructor::newInstance 上的性能稍慢。 在 32 个方法上使用 Method::invoke 的简单应用程序的冷启动时间从 64 毫秒增加到 70 毫秒。 我们将继续研究解决这些小问题的方法。
这种方法将降低升级对新语言功能的反射支持的成本,并进一步允许我们通过删除对MagicAccessorImpl子类的特殊处理来简化 HotSpot VM。
调用者敏感的方法
调用者敏感方法是一种其行为取决于其直接调用者的类的方法。调用者敏感方法的实现会进行堆栈遍历以找到其直接调用者,跳过内部反射机制引入的任何栈帧。
以下是平台中一些对调用者敏感的方法:
Class::forName(String)使用其调用者类的类加载器来加载命名类并在启用安全管理器时执行权限检查。Method::invoke、Constructor::newInstance、Field::getX和Field::setX对其调用者的类执行访问检查,除非通过setAccessible(true)抑制访问检查 .MethodHandles::lookup使用其调用者的类作为返回的Lookup对象的查找类。
此示例代码显示了如何通过Method::invoke调用对调用方敏感的方法CSM::returnCallerClass。
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 to be the caller class
var caller = m.invoke(null);
assert(caller == Foo.class);
}
}
首先,Method::invoke 发现 Foo 作为它的直接调用者进行检查。 它检查 Foo 是否可以访问 CSM::returnCallerClass。 然后它反射性地调用CSM::returnCallerClass。 由于 CSM::returnCallerClass 是一个调用者敏感的方法,它会找到它的直接调用者类,跳过反射栈帧,并返回它。 在这种情况下,CSM::returnCallerClass 发现 Foo 作为调用者类。 堆栈看起来像这样:
CSM.returnCallerClass
jdk.internal.reflect.NativeMethodAccessorImpl.invoke0
jdk.internal.reflect.NativeMethodAccessorImpl.invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke
java.lang.reflect.Method.invoke
Foo.test
:
:
请注意,查找调用者类的堆栈遍历进行了两次,一次用于Method::invoke,一次用于CSM::returnCallerClass。
对调用者敏感方法的方法句柄调用
如果请求获取调用者敏感方法的方法句柄,则适用字节码行为的一般规则,但它们以特殊方式查找类。 生成的方法句柄的行为就像是从包含在 lookup类 中的指令调用一样,因此调用者敏感的方法会检测lookup类。 (相比之下,方法句柄的调用者被忽略。)因此,在调用者敏感的方法的情况下,不同的lookup类可能会产生不同行为的方法句柄。
由于调用者敏感方法的这种行为,通过方法句柄调用的Method::invoke对目标调用者敏感方法的调用无法正常工作。 例如,Bar 通过链式反射调用调用 CSM::returnCallerClass,如下所示:
class Bar {
void test() throws Throwable {
//Method::invoke的方法句柄
MethodHandle mh = MethodHandles.lookup()
.findVirtual(Method.class, "invoke",
methodType(Object.class, Object.class, Object[].class));
// CSM::returnCallerClass的反射对象
Method m = CSM.class.getMethod("returnCallerClass");
//通过方法句柄和目标函数调用Method::invoke
// 被反射调用是CSM::returnCallerClass
var caller = mh.invoke(m, null, null);
assert(caller == Bar.class); // Fail!
}
}
可以合理地期望这个调用 CSM::returnCallerClass 的链式反射调用的行为应该与静态调用 CSM::returnCallerClass 时的行为相同,即 Bar 应该是返回的类。 然而,当前的实现返回了不正确的调用者类。
下面的堆栈显示了内部实现,包括隐藏的帧,它揭示了通过堆栈遍历找到的调用者类。 另一方面,Method::invoke 是通过方法句柄调用的。 Method::invoke 应该表现得好像它被 Lookup 对象的查找类调用,创建指定的方法句柄,即 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
:
:
此示例显示了当前实现中的错误:如果在依赖堆栈遍历查找调用者时通过链式反射调用调用,则两个调用者敏感的方法无法正常工作。
当前实现注入了一个隐藏类Bar$$InjectedInvoker/0x0000000800003400,它与Bar在同一个运行时包中,并且由与Bar相同的定义加载器定义,具有相同的保护域。堆栈遍历时会发现Bar$$InjectedInvoker/0x0000000800003400 作为调用者的类而不是 Bar。 这种方法适用于依赖于运行时包、定义加载器或调用者类的保护域的调用者敏感方法,但它不适用于需要确切调用者类的 MethodHandles::lookup 调用(参见 8013527和 8257874了解详情)。
调用者敏感方法的特殊调用序列
新的实现为调用者敏感的方法引入了一个特殊的调用序列。 调用者敏感的方法可以提供一个同名的私有适配器,但旁边有一个额外的 Class 参数。 当通过核心反射或方法句柄调用对调用者敏感的方法时,它会查找是否存在带有 Class 参数的适配器方法。 如果找到,它将使用调用者类参数调用适配器方法。 这个特殊的调用序列确保相同的调用者类通过Method::invoke、MethodHandle::invokeExact 或这些方法的混合传递给调用者敏感的方法。
例如,CSM::returnCallerClass 及其适配器方法将如下所示:
class CSM {
@CallerSensitive static Class<?> returnCallerClass() {
return returnCallerClass(Reflection.getCallerClass());
}
private static Class<?> returnCallerClass(Class<?> caller) {
return caller;
}
}
在新实现中,上述示例的堆栈如下所示:
CSM.returnCallerClass(caller) <--- adaptor 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
:
:
和
CSM.returnCallerClass(caller) <--- adaptor 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) <--- adaptor method
java.lang.invoke.DirectMethodHandle$Holder.invokeSpecial
java.lang.invoke.LambdaForm$MH/0x0000000800004000.invoke
java.lang.invoke.LambdaForm$MH/0x0000000800003c00.invoke_MT
Bar.test
:
:
CSM::returnCallerClass 和 Method::invoke 都可以有一个定义了调用者类参数的适配器方法。 Foo 调用 Method::invoke,它会遍历堆栈以找到调用者的类。它将调用者的类直接传递给CSM::returnCallerClass 的适配器方法。
类似地,Bar 通过方法句柄调用Method::invoke 来调用CSM::returnCallerClass。在这种情况下,MethodHandle::invokeExact 使用产生方法句柄的Lookup 对象的查找类作为调用者的类,因此不涉及堆栈遍历。查找类是Bar。它将以Bar作为调用者的类调用Method::invoke的适配器方法,进而调用以Bar作为调用者的CSM::returnCallerClass的适配器方法。当反射性调用调用者敏感方法时,新的实现消除了多次堆栈遍历的需要。
对于需要精确调用者类的调用者敏感方法,必须定义适配器方法以确保正确性。 MethodHandles::lookup 和 ClassLoader::registerAsParallelCapable 是 JDK 中仅有的两个需要确切调用者类的方法。
另一方面,对于使用调用者的类进行访问检查或安全权限检查的调用者敏感方法,即基于其运行时包、定义加载器或保护域,适配器方法是可选的。
新的实现将使用特殊的调用序列支持对调用者敏感的方法的反射调用,无论有没有适配器。
选项
选项 1:什么也不做
保留现有的核心反射实现以避免任何兼容性风险。 为反射生成的动态字节码将保留在类文件版本 49,虚拟机将继续特殊处理此类字节码。
我们拒绝这种选择,因为
- 更新
java.lang.reflect和java.lang.invoke以支持 Project Valhalla 的原始类和泛型专业化的成本会很高 - 可能需要虚拟机中的其他特殊规则来支持旧类文件格式限制内的新语言功能,以及
- Project Loom 需要找到一种方法来处理通过核心反射引入的原生堆栈帧。
选项 2: 升级到新的字节码库
替换反射使用的字节码编写器,以使用与类文件格式一起演化的新字节码库,但保留现有的反射实现并继续特殊处理动态生成的反射字节码。
这种替代方案的兼容性风险比我们上面提出的要低,但它仍然是一个相当大的工作量,它仍然具有第一个替代方案的第一个和最后一个缺点。
测试
全面的测试将确保新的实现具有健壮性的并与现有行为兼容。性能测试将确保与当前实现相比没有显着的性能回归。 我们将鼓励开发人员使用早期访问版本来测试尽可能多的库和框架,以帮助我们识别任何行为或性能回归。
在许多情况下,微基准测试没有显示出明显的性能回归和改进。 我们将继续探索提高性能的机会。
基准
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
新的实现
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
风险和假设
高度依赖现有实现的和未记录的代码可能会受到影响。 为了减轻这种兼容性风险,作为一种变通方法,您可以通过-Djdk.reflect.useDirectMethodHandle=false 启用旧的实现。
- 内部生成的反射类(即
MagicAccessorImpl的子类)的代码将不再有效,必须更新。 - 方法句柄调用可能比旧的反射实现消耗更多的资源。 此类调用涉及调用多个 Java 方法以确保在访问之前初始化成员的声明类,因此可能需要更多堆栈空间用于必要的执行帧。 这可能会导致
StackOverflowError,或,如果在初始化类时抛出StackOverflowError,则会导致NoClassDefFoundError。