Java18中用方法句柄替代以前的反射机制


Java 17及更早的反射实现依赖于委托模式--特别是一个名为DelegatingMethodAccessorImpl的类。该类的委托开始时是一个依赖本地代码执行反射调用的类。
然而,一旦通过了阈值,该委托就会被一个自定义类所取代(据说它已被打补丁了)。
这个自定义类是在运行时动态创建的,这是一个相对昂贵的操作,这就是为什么在调用阈值通过之前不执行该操作的原因。

这种实现有时被称为 "膨胀实现",因为本机委托被膨胀为自定义字节码实现,无需本机调用。
这样做效果很好,但膨胀实现相当复杂,因为它有两个独立的代码路径用于反射--一个用于本地方法,另一个用于自定义类--它们都是动态生成的字节码存根。

此外,还有一些Unsafe类的使用,您最好尽量减少或删除。

不仅如此,由于字节码验证的复杂性,动态生成的反射方法访问器需要由JVM通过MagicAccessorImpl类进行特殊处理。这就给已经有些古怪的系统增加了更多的复杂性。

总之,这些细节意味着有大量代码需要维护,尤其当要与新的MethodHandles API一起维护时,后者也为反射提供了类似的功能。

这引出了一个有趣的可能性:如果可以用基于方法句柄的等效功能来替代整个膨胀实现会怎样呢?
这个想法的相关计划是 JEP 416:使用方法句柄重新实现核心反射( JEP 416: Reimplement core reflection with method handles),该计划已在 Java 18 中发布。

本文接下来将深入探讨新的实现,并将其与现有的膨胀实现进行对比,首先将研究相关的安全机制。

方法句柄可以用来替代反射
在现代Java代码中,方法句柄可以用来替代反射,因为它们具有基本相同的功能。它们之间存在一些差异--方法名称的变化,尤其是查找对象的使用--但总的来说,已经熟悉反射的Java程序员在使用方法句柄时应该不会遇到任何严重的困难。

所有这些都提出了一个引人入胜的可能性--方法句柄是否可以用作执行反省操作的引擎,并在其上加装旧的Reflection API?

是的!

事实上,JDK 18确实在不改变接口的情况下使用方法句柄实现了反射

旧的(Java 1.1)反射接口必须保留,因为Java总是试图保留向后兼容性。

MethodAccessor接口被保留下来,但java.lang.reflect.Method不再是您已经熟悉的MethodAccessorImpl类,而是包含了方法句柄的证据,正如您在源代码中看到的那样:

public MethodAccessor newMethodAccessor(Method method, boolean callerSensitive) {
    // use the root Method that will not cache caller class
    Method root = langReflectAccess.getRoot(method);
    if (root != null) {
        method = root;
    }

    if (useMethodHandleAccessor()) {
        return MethodHandleAccessorFactory.newMethodAccessor(method, callerSensitive);
    } else {
        if (noInflation() && !method.getDeclaringClass().isHidden()) {
            return generateMethodAccessor(method);
        } else {
            NativeMethodAccessorImpl acc = new NativeMethodAccessorImpl(method);
            return acc.getParent();
        }
    }
}


方法句柄实现是默认的,因此 useMethodHandleAccessor() 将默认为 true,但旧的膨胀实现暂时仍然可用。它可以通过下面的开关激活,只是这纯粹是出于兼容性的原因,并将在未来的 JDK 版本中删除:

-Djdk.reflect.useDirectMethodHandle=false

不幸的是,在Java代码中证明新实现的存在并不容易。正如您在本文前面所看到的,最近发布的 Java 版本已经开始从内省返回的定义字段列表中删除某些字段。这样做的结果是无法以反射方式访问这些字段。

最后,谈谈性能:您可能很想提出这样一个问题:"新的实现与原始的、膨胀的代码相比如何?这并没有一个简单、明确的答案。反射子系统的许多方面都发生了变化(例如访问控制、装箱和对访问器的易失性访问),总体效果无法推理。

请记住性能分析的核心是一个有点平凡的事实:如果您想了解特定语言或 JVM 功能在一定输入范围内对特定应用程序的性能影响,您必须测试特定应用程序。