跳到主要内容

JEP 371:隐藏类

QWen Max 中英对照 JEP 371: Hidden Classes

总结

引入了隐藏类,这些类不能被其他类的字节码直接使用。隐藏类旨在供在运行时生成类并间接通过反射使用它们的框架使用。隐藏类可以定义为访问控制巢的成员,并且可以独立于其他类进行卸载。

目标

  • 允许框架将类定义为框架的非可发现实现细节,这样其他类就无法链接到这些类,也无法通过反射发现它们。

  • 支持使用非可发现类扩展访问控制组。

  • 支持积极卸载非可发现类,以便框架能够灵活地定义所需数量的类。

  • 弃用非标准 API sun.misc.Unsafe::defineAnonymousClass,并计划在未来的版本中弃用以进行移除。

  • 不以任何方式更改 Java 编程语言。

非目标

  • 目标并非支持 sun.misc.Unsafe::defineAnonymousClass 的所有功能,例如常量池修补。

动机

许多构建在 JVM 上的语言实现依赖于动态类生成来实现灵活性和效率。例如,在 Java 语言中,javac 不会在编译时将 lambda 表达式转换为专用的 class 文件,而是生成字节码,该字节码会动态生成并实例化一个类,以便在需要时生成与 lambda 表达式对应的对象。同样,非 Java 语言的运行时环境通常通过使用动态代理 来实现这些语言的高阶特性,动态代理也会动态生成类。

语言实现者通常希望动态生成的类在逻辑上属于静态生成类的实现部分。这一意图表明,动态生成的类应具备以下一些理想的特性:

  • 不可发现性。能够通过名称独立发现不仅是不必要的,而且是有害的。这会破坏动态生成类仅仅是静态生成类实现细节的目标。

  • 访问控制。可能需要扩展现有静态生成类的访问控制上下文,以包含动态生成的类。

  • 生命周期。动态生成的类可能仅在有限的时间内需要,因此在静态生成类的整个生命周期中保留它们可能会不必要地增加内存占用。针对这种情况的现有解决方法(例如每个类的类加载器)既繁琐又低效。

遗憾的是,定义类的标准 API——ClassLoader::defineClassLookup::defineClass——并不区分类的字节码是动态生成(在运行时)还是静态生成(在编译时)。这些 API 始终定义一个 可见的 类,每当同一加载器层次结构中的其他类尝试链接该名称的类时都会使用它。因此,该类可能比预期更容易被发现或具有更长的生命周期。此外,这些 API 只能定义作为嵌套成员的类,如果嵌套的宿主类事先知道成员类的名称;从实际操作上讲,这阻止了动态生成的类成为嵌套的成员。

如果一个标准 API 能够定义那些无法被发现且生命周期有限的隐藏类,那么 JDK 内外动态生成类的框架就可以改为定义隐藏类。这将提高所有基于 JVM 的语言实现的效率。例如:

  • java.lang.reflect.Proxy 可以定义隐藏类来充当实现代理接口的代理类;

  • java.lang.invoke.StringConcatFactory 可以生成隐藏类来保存常量连接方法;

  • java.lang.invoke.LambdaMetaFactory 可以生成隐藏的嵌套类来保存访问外围变量的 lambda 表达式主体;以及

  • JavaScript 引擎可以为从 JavaScript 程序翻译过来的字节码生成隐藏类,同时知道这些类将在引擎不再使用它们时被卸载。

描述

Java 7 中引入的 Lookup API 允许一个类获取一个*查找对象,该对象提供对类、方法和字段的反射访问。关键在于,无论什么代码最终使用了查找对象,反射访问始终发生在最初获取该查找对象的类(即查找类*)的上下文中。实际上,查找对象将其查找类的访问权限传递给了接收该对象的任何代码。

Java 9 通过引入 Lookup::defineClass(byte[]) 方法增强了查找对象的传输能力。该方法根据提供的字节定义一个与最初获取查找对象的类相同上下文中的新类。也就是说,新定义的类具有与查找类相同的定义类加载器、运行时包和保护域。

这个 JEP 提议扩展 Lookup API,以支持定义只能通过反射访问的隐藏类。隐藏类在字节码链接期间不能被 JVM 发现,也不能被显式利用类加载器的程序发现(例如,通过 Class::forNameClassLoader::loadClass)。当隐藏类不再可访问时,它可以被卸载,或者它可以与类加载器共享生命周期,这样只有当类加载器被垃圾回收时它才会被卸载。可选地,隐藏类可以作为访问控制巢的一员被创建。

为了简洁起见,本 JEP 提到“隐藏类”时,应理解为隐藏的类或接口。同样,“普通类”是指普通类或接口,即 ClassLoader::defineClass 的结果。

创建隐藏类

而普通类是通过调用 ClassLoader::defineClass 创建的,隐藏类则是通过调用 Lookup::defineHiddenClass 创建的。这会使 JVM 根据提供的字节派生出一个隐藏类,链接该隐藏类,并返回一个提供对该隐藏类进行反射访问的查找对象。调用程序应谨慎存储该查找对象,因为它是获取隐藏类的 Class 对象的唯一方式。

所提供的字节必须是一个 ClassFile 结构(JVMS 4.1)。通过 Lookup::defineHiddenClass 衍生隐藏类的过程与通过 ClassLoader::defineClass 衍生普通类的过程类似,但有一个主要区别将在下文讨论。隐藏类被衍生后,它会像普通类一样进行链接(JVMS 5.4),只是不会施加任何加载约束。隐藏类链接完成后,如果 Lookup::defineHiddenClassinitialize 参数为 true,则会对其进行初始化;如果该参数为 false,则当反射方法实例化该隐藏类或访问其成员时,隐藏类将被初始化。

隐藏类的创建方式主要区别在于其所赋予的名称。隐藏类并非匿名类。 它具有一个可通过 Class::getName 获取的名称,并可能在诊断信息中显示(例如 java -verbose:class 的输出)、在 JVM TI 类加载事件、JFR 事件以及堆栈跟踪中显示。然而,该名称的形式足够特殊,使得该类对所有其他类而言实际上是不可见的。该名称是以下内容的拼接:

  1. ClassFile 结构中 this_class 指定的内部形式的二进制名称(JVMS 4.2.1),例如 A/B/C

  2. '.' 字符;以及

  3. 由 JVM 实现选择的一个非限定名称(JVMS 4.2.2)。

例如,如果 this_class 指定了 com/example/Foo(二进制名称 com.example.Foo 的内部形式),那么从 ClassFile 结构派生的隐藏类可能被命名为 com/example/Foo.1234。这个字符串既不是二进制名称,也不是二进制名称的内部形式。

给定一个隐藏类,其名称为 A/B/C.xClass::getName 的结果是以下内容的连接:

  1. 二进制名称 A.B.C(通过获取 A/B/C 并将每个 '/' 替换为 '.' 获得);
  2. 字符 '/';以及
  3. 非限定名称 x

例如,如果一个隐藏类被命名为 com/example/Foo.1234,那么 Class::getName 的结果是 com.example.Foo/1234。同样,这个字符串既不是二进制名称,也不是二进制名称的内部形式。

隐藏类的命名空间与普通类的命名空间是不相交的。给定一个 ClassFile 结构,其中 this_class 指定了 com/example/Foo/1234,调用 cl.defineClass("com.example.Foo.1234", bytes, ...) 仅仅会生成一个名为 com.example.Foo.1234 的普通类,它与名为 com.example.Foo/1234 的隐藏类不同。不可能创建一个名为 com.example.Foo/1234 的普通类,因为 cl.defineClass("com.example.Foo/1234", bytes, ...) 会拒绝该字符串参数,认为它不是一个二进制名称。

我们承认,不使用二进制名称作为隐藏类的名称可能会带来问题,但这与长期以来 Unsafe::defineAnonymousClass 的做法兼容(参见此处的讨论)。在 Class::getName 输出中使用 / 来表示隐藏类,也与堆栈跟踪中使用 / 通过定义模块和加载器来限定类的方式保持风格一致(参见 StackTraceElement::toString)。下面的错误日志揭示了两个隐藏类,它们都位于模块 m1 中:一个隐藏类具有方法 test,另一个隐藏类具有方法 apply

java.lang.Error: thrown from hidden class com.example.Foo/0x0000000800b7a470
at m1/com.example.Foo/0x0000000800b7a470.toString(Foo.java:16)
at m1/com.example.Foo_0x0000000800b7a470$$Lambda$29/0x0000000800b7c040.apply(<Unknown>:1000001)
at m1/com.example.Foo/0x0000000800b7a470.test(Foo.java:11)

隐藏类与类加载器

尽管隐藏类有一个对应的 Class 对象,并且隐藏类的超类型是由类加载器创建的,但在隐藏类本身的创建过程中并没有类加载器的参与。请注意,这个 JEP(Java 改进提案)从未说过隐藏类是“被加载的”。 没有类加载器被记录为隐藏类的初始加载器,并且不会生成涉及隐藏类的加载约束。因此,任何类加载器都不知道隐藏类的存在:对于任何 DCN 的值,类 D 的运行时常量池中的符号引用 N 指代类 C 时,永远不会解析为隐藏类。反射方法 Class::forNameClassLoader::findLoadedClassLookup::findClass 都无法找到隐藏类。

尽管与类加载器分离,隐藏类仍被认为具有一个定义类加载器。这是为了解析隐藏类自身字段和方法所使用的类型。特别是,隐藏类与查找类具有相同的定义类加载器、运行时包和保护域,查找类是指最初获取调用 Lookup::defineHiddenClass 的查找对象的类。

使用隐藏类

Lookup::defineHiddenClass 返回一个 Lookup 对象,其查找类是新创建的隐藏类。可以通过在返回的 Lookup 对象上调用 Lookup::lookupClass 来获取隐藏类的 Class 对象。通过该 Class 对象,可以实例化隐藏类并访问其成员,就像操作普通类一样,但存在四个限制:

  1. Class::getName 返回一个字符串,该字符串不是一个二进制名称,如前所述。

  2. Class::getCanonicalName 返回 null,表示隐藏类没有规范名称。(请注意,Java 语言中匿名类的 Class 对象具有相同的行为。)

  3. 隐藏类中声明的 final 字段不可修改。对隐藏类的 final 字段调用 Field::set 和其他 setter 方法将抛出 IllegalAccessException,而不管字段的 accessible 标志 如何。

  4. Class 对象不能被 检测代理 修改,也不能被 JVM TI 代理重新定义或转换。然而,我们将扩展 JVM TI 和 JDI 以支持隐藏类,例如测试某个类是否为隐藏类、在任何“已加载”类的列表中包含隐藏类,以及在创建隐藏类时发送 JVM TI 事件。

重要的是要认识到,其他类使用隐藏类的唯一方法是通过其 Class 对象间接进行。隐藏类不能被其他类中的字节码指令直接使用,因为它无法被名义上引用,即通过名称引用。例如,假设一个框架得知了一个名为 com.example.Foo/1234 的隐藏类,并生成了一个试图实例化该隐藏类的 class 文件。class 文件中的代码将包含一个 new 指令,该指令最终指向表示该名称的常量池条目。如果该框架试图将名称表示为 com/example/Foo.1234,那么这个 class 文件将是无效的 -- 因为 com/example/Foo.1234 不是一个有效的二进制名称的内部形式。另一方面,如果该框架试图以有效的内部形式 com/example/Foo/1234 来表示该名称,那么 JVM 将通过首先将内部形式的名称转换为二进制名称 com.example.Foo.1234 来解析常量池条目,然后尝试加载该名称的类;这很可能会失败,并且肯定不会找到名为 com.example.Foo/1234 的隐藏类。隐藏类并非真正匿名,因为它的名称是暴露的,但它实际上却是不可见的。

如果没有常量池名义上引用隐藏类的能力,就无法将隐藏类用作超类、字段类型、返回类型或参数类型。这种可用性的缺乏让人联想到 Java 语言中的匿名类,但隐藏类更进一步:匿名类可以封装其他类,以便让它们访问其成员,但隐藏类不能封装其他类(它们的 InnerClasses 属性不能命名它)。即使是隐藏类本身,也无法在自己的字段和方法声明中将自己用作字段类型、返回类型或参数类型。

重要的是,隐藏类中的代码可以直接使用隐藏类,而无需依赖 Class 对象。这是因为隐藏类中的字节码指令可以符号化地(无需关心其名称)而不是名义上引用隐藏类。例如,隐藏类中的 new 指令可以通过常量池条目直接实例化隐藏类,该条目直接引用当前 ClassFile 中的 this_class 项。其他指令,例如 getstaticgetfieldputstaticputfieldinvokestaticinvokevirtual,也可以通过相同的常量池条目访问隐藏类的成员。隐藏类内部的直接使用非常重要,因为它简化了语言运行时和框架对隐藏类的生成。

隐藏类通常与普通类具有相同的反射能力。也就是说,隐藏类中的代码可以定义普通类和隐藏类,并且可以通过它们的 Class 对象来操作普通类和隐藏类。隐藏类甚至可以充当查找类。也就是说,隐藏类中的代码可以获得其自身的查找对象,这有助于处理隐藏的嵌套类(见下文)。

堆栈跟踪中的隐藏类

默认情况下,隐藏类的方法不会显示在堆栈跟踪中。它们代表了语言运行时的实现细节,预计对于诊断应用程序问题的开发者来说永远不会有所帮助。然而,可以通过选项 -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames 将其包含在堆栈跟踪中。

有三个用于具体化堆栈跟踪的 API:Throwable::getStackTraceThread::getStackTrace 以及 Java 9 中引入的较新的 StackWalker API。对于 Throwable::getStackTraceThread::getStackTrace API,默认情况下隐藏类的堆栈帧会被省略;可以通过与上述堆栈跟踪相同的选项将它们包含进来。对于 StackWalker API,只有在设置了 SHOW_HIDDEN_FRAMES 选项时,JVM 实现才应包含隐藏类的堆栈帧。这允许堆栈跟踪过滤在开发者诊断应用程序问题时省略不必要的信息

访问控制嵌套中的隐藏类

根据 JEP 181 的定义,Java 11 引入了“嵌套(nest)”的概念。嵌套是一组类的集合,这些类可以互相访问彼此的私有成员,但无需使用通常与 Java 语言中嵌套类相关的任何后门式扩展访问权限方法。这个集合是静态定义的:其中一个类充当嵌套主机(nest host),它的类文件列出了作为嵌套成员的其他类;反过来,嵌套成员也会在其类文件中指明哪个类是嵌套的主机。虽然静态成员关系对于从 Java 源代码生成的类文件非常有效,但对于由语言运行时动态生成的类文件来说,通常并不足够。为了帮助这些运行时,并鼓励使用 Lookup::defineHiddenClass 而不是 Unsafe::defineAnonymousClass,隐藏类可以在运行时加入一个嵌套;而普通类则不能。

通过向 Lookup::defineHiddenClass 传递 NESTMATE 选项,可以创建作为现有 nest 成员的隐藏类。隐藏类所加入的 nest 并不由传递给 Lookup::defineHiddenClass 的参数决定。相反,要加入的 nest 是根据查找类(lookup class)推断出来的,也就是说,从最初获取查找对象的代码所属的类推断出来:隐藏类与查找类属于同一个 nest(见下文)。

为了使 Lookup::defineHiddenClass 将隐藏类添加到嵌套中,查找对象必须具有适当的权限,即 PRIVATEMODULE 访问权限。这些权限表明查找对象是由查找类获取的,目的是允许其他代码扩展嵌套。

JVM 不允许嵌套的巢。一个巢的成员不能充当另一个巢的宿主,无论巢成员资格是静态定义的还是动态定义的。

如果查找类是一个普通类,其嵌套成员身份可以通过 NestHost 静态指示,或者如果查找类是一个隐藏类,则可能已被动态设置。静态嵌套成员资格会延迟验证。对于语言运行时或框架库来说,能够将隐藏类添加到可能存在不良嵌套成员关系的查找类的嵌套中是非常重要的。例如,考虑在 Java 8 中引入的 LambdaMetaFactory 框架。当类 C 的源代码包含一个 lambda 表达式时,相应的 C.class 文件会在运行时使用 LambdaMetaFactory 来定义一个隐藏类,该隐藏类保存了 lambda 表达式的主体并实现了所需的函数式接口。C.class 可能具有一个错误的 NestHost 属性,但 C 的执行从未引用 NestHost 属性中命名的类 H。由于 lambda 表达式的主体可能访问 Cprivate 成员,因此隐藏类也需要能够访问它们;因此,LambdaMetaFactory 尝试将隐藏类定义为由 C 托管的嵌套的成员。

假设我们有一个查找类 C,并且使用 NESTMATE 选项调用 defineHiddenClass 来创建一个隐藏类并将其添加到 C 的嵌套结构中。隐藏类的嵌套宿主按以下方式确定:

  • 如果 C 是一个普通类且缺少 NestHost 属性,那么 C 是其自身的宿主,同时也是隐藏类的巢宿主。
  • 如果 C 是一个拥有有效 NestHost 属性(名为 H)的普通类,那么 C 的巢宿主 H 就是隐藏类的巢宿主。在这种情况下,隐藏类被添加为 H 的巢成员。
  • 如果 C 是一个拥有无效 NestHost 属性的普通类,那么 C 被用作隐藏类的巢宿主。
  • 如果 C 是一个在没有 NESTMATE 选项的情况下创建的隐藏类,那么 C 是其自身的宿主,同时也是隐藏类的巢宿主。
  • 如果 C 是一个使用 NESTMATE 选项创建并动态添加到 D 的巢中的隐藏类,那么 D 的巢宿主被用作隐藏类的巢宿主。

如果创建隐藏类时没有使用 NESTMATE 选项,那么该隐藏类将成为其自身嵌套结构的宿主。这符合以下策略:每个类要么作为某个嵌套结构的成员,并以另一个类为嵌套宿主;要么自身就是某个嵌套结构的宿主。隐藏类可以创建额外的隐藏类作为其嵌套结构的成员:隐藏类中的代码首先会获取一个指向自身的查找对象,然后在该对象上调用 Lookup::defineHiddenClass 并传递 NESTMATE 选项。

对于作为嵌套成员创建的隐藏类的 Class 对象,Class::getNestHostClass::isNestmateOf 将按预期工作。Class::getNestMembers 可以在嵌套中任何类的 Class 对象上调用——无论是成员还是宿主,无论是普通类还是隐藏类——但只返回静态定义的成员(即,宿主中通过 NestMembers 枚举的普通类)以及嵌套宿主。

Class::getNestMembers 不包括动态添加到嵌套中的隐藏类,因为隐藏类是不可发现的,并且应该只与创建它们的代码相关,这些代码已经知道了嵌套成员关系。这防止了隐藏类通过嵌套成员关系泄露,如果它们本应保持私有的话。

卸载隐藏类

由类加载器定义的类与该类加载器有着密切的关系。特别是,每个 Class 对象都有一个指向定义它的 ClassLoader 的引用(参考此处)。这告诉 JVM 在解析类中的符号时应使用哪个加载器。这种关系的一个结果是,普通类无法被卸载,除非其定义加载器可以被垃圾回收器回收(JLS 12.7)。能够回收定义加载器意味着没有对该加载器的活动引用,反过来也意味着没有对该加载器所定义的任何类的活动引用。(如果这些类是可访问的,它们将引用加载器。)这种普遍缺乏活跃引用的状态是唯一可以安全卸载普通类的情况。

因此,为了最大限度地增加卸载普通类的机会,尽量减少对类及其定义加载器的引用非常重要。语言运行时通常通过创建多个类加载器来实现这一点,每个加载器专门用于定义一个类,或者可能是少量相关类。当某个类的所有实例都被回收,并且假设运行时不保留对该类加载器的引用时,该类及其定义加载器都可以被回收。然而,这导致大量的类加载器对内存提出了很高的要求。此外,根据微基准测试,ClassLoader::defineClassUnsafe::defineAnonymousClass 明显要慢得多。

隐藏类不是由类加载器创建的,与被认为是其定义加载器的类加载器仅有松散的连接。我们可以利用这些事实,允许即使名义上的定义加载器无法被垃圾回收器回收时,隐藏类仍然可以被卸载。只要存在对隐藏类的活动引用——无论是对隐藏类实例的引用,还是对其 Class 对象的引用——隐藏类就会使其名义上的定义加载器保持活动状态,以便 JVM 可以使用该加载器解析隐藏类中的符号。然而,当指向隐藏类的最后一个活动引用消失时,加载器不必通过保持隐藏类的活动状态来“回报”它。

在定义该类的加载器可访问的情况下卸载一个普通类是不安全的,因为 JVM 或使用反射的代码可能会要求该加载器重新加载这个类,即加载一个同名的类。当静态初始化程序第二次运行时,这可能会产生不可预测的影响。而卸载隐藏类则无需担心此类问题,因为隐藏类的创建方式不同。由于隐藏类的名称是 Lookup::defineHiddenClass 的输出,而不是输入,因此无法重新创建之前已卸载的“相同”隐藏类。

默认情况下,Lookup::defineHiddenClass 会创建一个隐藏类,该类可以被卸载,无论其名义上的定义加载器是否仍然存活。也就是说,当隐藏类的所有实例都被回收且隐藏类不再可访问时,即使其名义上的定义加载器仍然可访问,隐藏类也可能被卸载。这种行为在语言运行时创建一个隐藏类以服务于由任意类加载器定义的多个类时非常有用:与 ClassLoader::defineClassUnsafe::defineAnonymousClass 相比,运行时将会看到占用空间和性能的提升。在其他情况下,语言运行时可能会将隐藏类链接到仅一个普通类,或者可能是具有与隐藏类相同定义加载器的少量普通类。在这种情况下,隐藏类必须与普通类共存,可以将 STRONG 选项传递给 Lookup::defineHiddenClass。这会安排隐藏类与其名义上的定义加载器之间具有与普通类与其定义加载器相同的强关系,也就是说,只有在其名义上的定义加载器可以被回收时,隐藏类才会被卸载。

替代方案

除了现有的为代理类生成包私有访问桥以访问目标类的私有成员这一权宜之计外,运行时注入一个嵌套类没有其他替代方法。如果一个类对某个类加载器可见,那么没有其他方法可以将其对该类隐藏。

测试

  • 我们将更新 LambdaMetaFactoryStringConcatFactoryLambdaForms 以使用新的 API。性能测试将确保 lambda 链接或字符串连接没有回归。

  • 新 API 的单元测试将会被开发。

风险与假设

我们假设当前使用 Unsafe::defineAnonymousClass 的开发者能够轻松迁移到 Lookup::defineHiddenClass。开发者应该注意,相对于 VM 匿名类,隐藏类的功能存在三个小的限制。

  • 受保护的访问. 令人惊讶的是,一个 VM 匿名类可以访问其宿主类的 protected 成员,即使该 VM 匿名类存在于不同的运行时包中,并且不是宿主类的子类。相比之下,隐藏类会正确应用访问控制规则:隐藏类只能在与另一类相同的运行时包中,或者是该类的子类时,才能访问另一类的 protected 成员。隐藏类对查找类的 protected 成员没有特殊访问权限。

  • 常量池修补. 一个 VM 匿名类可以在定义时将其常量池条目解析为具体的值。这使得关键常量可以在 VM 匿名类和定义它的语言运行时之间共享,也可以在多个 VM 匿名类之间共享。例如,语言运行时通常会在其地址空间中有 MethodHandle 对象,这些对象对于新定义的 VM 匿名类非常有用。运行时无需将这些对象序列化为 VM 匿名类中的常量池条目,然后在这些类中生成字节码来费力地 ldc 这些条目,而是可以直接向 Unsafe::defineAnonymousClass 提供对其活动对象的引用。新定义的 VM 匿名类的相关常量池条目会预先链接到这些对象,从而提高性能并减少占用空间。此外,这还允许 VM 匿名类相互引用:类文件中的常量池条目是基于名称的,因此它们无法引用无名的 VM 匿名类。然而,语言运行时可以轻松跟踪其 VM 匿名类的活动 Class 对象,并将它们提供给 Unsafe::defineAnonymousClass,从而预先链接新类的常量池条目到其他 VM 匿名类。Lookup::defineHiddenClass 方法不会有这些能力,因为未来的增强功能可能会提供统一适用于所有类的常量池条目预链接。

  • 自我优化控制. VM 匿名类的设计假设是只有 JDK 代码会定义它们。因此,VM 匿名类具有一种以前只有 JDK 中的类才具备的不寻常能力,即通过 HotSpot JVM 控制自身的优化。这种控制是通过 VM 匿名类定义字节中的注解属性来实现的:@ForceInline@DontInline 会使 HotSpot 总是内联或从不内联某个方法,而 @Stable 会使 HotSpot 将非空字段视为可折叠常量。然而,由 JDK 代码动态定义的 VM 匿名类很少需要这种能力。甚至有可能未来的改进会使这些优化变得过时。因此,即使是由 JDK 代码定义的隐藏类也不会有控制自身优化的能力。(这被认为不会对从定义 VM 匿名类迁移到定义隐藏类的 JDK 代码带来任何风险。)

作为一个相关事项,VM-匿名类可以使用 @Hidden 注解来防止其方法出现在堆栈跟踪中。当然,此功能对隐藏类是自动的,并且将来可能会提供给其他类

迁移应该考虑以下因素:

  • 要从隐藏类中的代码调用私有嵌套实例方法,请使用 invokevirtualinvokeinterface 而不是 invokespecial。生成的字节码如果使用 invokespecial 调用私有嵌套实例方法将无法通过验证。invokespecial 应仅用于调用私有嵌套构造函数。

  • 如前所述,在隐藏类的 Class 对象上调用 getName 会返回一个非二进制名称的字符串,因为它包含 / 字符。用户级代码预计不会接触到这样的 Class 对象,但假定每个类都有一个二进制名称的框架级代码可能需要更新以处理隐藏类。先前为处理 VM 匿名类而更新过的框架级代码将继续有效,因为隐藏类使用与 VM 匿名类相同的命名约定。

  • JVM TI GetClassSignature 返回 JNI 样式的签名,并返回一个内部形式中并非二进制名称的字符串,例如包含 . 字符的字符串。假定每个类都有二进制名称的 JVM TI 代理和工具可能需要更新以处理隐藏类。另一方面,JDI 实现已经更新以处理隐藏类。隐藏类不能被 JVM TI 代理修改。受隐藏类类签名影响的工具应该是有限的。

依赖

JEP 181(基于嵌套的访问控制) 引入了基于嵌套的访问控制上下文,在该上下文中,一个嵌套中的所有类和接口在其嵌套成员之间共享私有访问权限。