JEP 371: Hidden Classes | 隐藏类
摘要
介绍 隐藏类(hidden classes),即无法被其他类的字节码直接使用的类。隐藏类旨在供框架在运行时生成类并通过反射间接使用。隐藏类可以作为 访问控制嵌套 的成员进行定义,并且可以独立于其他类进行卸载。
目标
允许框架将类定义为框架的非可发现实现细节,以便其他类无法链接到这些类,也无法通过反射发现它们。
支持通过非可发现类扩展访问控制嵌套。
支持非可发现类的积极卸载,以便框架能够根据需要定义任意多的类。
弃用 非标准 API
sun.misc.Unsafe::defineAnonymousClass
,目的是在未来的版本中弃用并移除它。不以任何方式更改 Java 编程语言。
非目标
- 不支持
sun.misc.Unsafe::defineAnonymousClass
的所有功能(如常量池修补)并不是目标。
动机
许多基于 JVM 的语言实现都依赖于动态类生成来实现灵活性和效率。例如,在 Java 语言的情况下,javac
在编译时不会将 lambda 表达式转换为专用的 class
文件,而是发出字节码以 动态生成并实例化一个类,以便在需要时生成与 lambda 表达式相对应的对象。类似地,非 Java 语言的运行时通常通过使用 动态代理 来实现这些语言的高级特性,这些代理也会动态生成类。
语言实现者通常希望动态生成的类是静态生成类的实现逻辑的一部分。这一意图表明动态生成的类具有一些理想的属性:
不可发现性。通过名称独立发现动态生成的类不仅不必要,而且有害。这破坏了动态生成的类仅仅是静态生成类的实现细节的目标。
访问控制。可能希望将静态生成类的访问控制上下文扩展到包括动态生成的类。
生命周期。动态生成的类可能只需要在有限的时间内存在,因此为静态生成类的整个生命周期保留它们可能会不必要地增加内存占用。针对这种情况的现有解决方案(如每个类的类加载器)既繁琐又低效。
不幸的是,定义类的标准 API——ClassLoader::defineClass
和 Lookup::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 以支持定义只能通过反射访问的 隐藏类。隐藏类在字节码链接过程中或在程序明确使用类加载器(例如,通过 Class::forName
和 ClassLoader::loadClass
)时,都无法被 JVM 发现。当隐藏类不再可达时,它可以被卸载,或者它可以与类加载器共享生命周期,以便仅在类加载器被垃圾回收时才被卸载。可选地,隐藏类可以作为访问控制嵌套的一个成员来创建。
为简洁起见,此 JEP 提到“隐藏类”,但应理解为隐藏类或接口。类似地,“正常类”表示正常类或接口,是 ClassLoader::defineClass
的结果。
创建隐藏类
正常类是通过调用 ClassLoader::defineClass
创建的,而隐藏类则是通过调用 Lookup::defineHiddenClass
创建的。这会导致 JVM 从提供的字节中派生出一个隐藏类,链接该隐藏类,并返回一个提供对隐藏类的反射访问的查找对象。调用程序应该仔细存储这个查找对象,因为这是获取隐藏类 Class
对象的唯一方式。
提供的字节必须是 ClassFile
结构(JVMS 4.1)。Lookup::defineHiddenClass
通过提供的字节派生隐藏类的方式与 ClassLoader::defineClass
通过提供的字节派生正常类的方式类似,但有一个主要差异,将在下面讨论。在隐藏类被派生后,它就像正常类一样被链接(JVMS 5.4),只是不施加加载约束。在隐藏类被链接后,如果 Lookup::defineHiddenClass
的 initialize
参数为 true
,则初始化该隐藏类;如果参数为 false
,则在通过反射方法实例化隐藏类或访问其成员时,该隐藏类将被初始化。
隐藏类的创建方式的主要差异在于其名称。隐藏类不是匿名的。它有一个可以通过 Class::getName
获得的名字,并可能显示在诊断信息中(如 java -verbose:class
的输出)、JVM TI 类加载事件、JFR 事件和堆栈跟踪中。但是,这个名称具有足够不寻常的形式,使得它实际上对其他所有类都是不可见的。该名称是以下内容的拼接:
ClassFile
结构中由this_class
指定的内部形式的二进制名称,例如A/B/C
;'.'
字符;JVM 实现选择的非限定名(JVMS 4.2.2)。
例如,如果 this_class
指定了 com/example/Foo
(二进制名称 com.example.Foo
的内部形式),那么从 ClassFile
结构派生的隐藏类可能被命名为 com/example/Foo.1234
。这个字符串既不是二进制名称,也不是二进制名称的内部形式。
给定一个隐藏类,其名称是 A/B/C.x
,Class::getName
的结果将是以下内容的拼接:
- 二进制名称
A.B.C
(通过将A/B/C
中的每个'/'
替换为'.'
获得); '/'
字符;- 非限定名
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
的方法。txtjava.lang.Error: 从隐藏类 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)
1
2
3
4
隐藏类和类加载器
尽管隐藏类有对应的 Class
对象,并且隐藏类的超类型是由类加载器创建的,但隐藏类本身的创建并不涉及类加载器。请注意,这个 JEP 从未说过隐藏类被“加载”。 没有类加载器被记录为隐藏类的启动加载器,也不会生成涉及隐藏类的加载约束。因此,任何类加载器都不知道隐藏类的存在:类 D
的运行时常量池中对由 N
表示的类 C
的符号引用,对于任何 D
、C
和 N
的值,都不会解析为隐藏类。反射方法 Class::forName
、ClassLoader::findLoadedClass
和 Lookup::findClass
不会找到隐藏类。
尽管与类加载器相分离,但 隐藏类被认为具有定义它的类加载器。这是解决隐藏类自己的字段和方法所使用的类型所必需的。特别是,隐藏类具有与查找类相同的定义类加载器、运行时包和保护域,查找类是原先获取在其上调用 Lookup::defineHiddenClass
方法的查找对象的类。
使用隐藏类
Lookup::defineHiddenClass
返回一个 Lookup
对象,其查找类是新创建的隐藏类。可以通过在返回的 Lookup
对象上调用 Lookup::lookupClass
来获取隐藏类的 Class
对象。通过 Class
对象,隐藏类可以像普通类一样被实例化并访问其成员,但有以下四个限制:
Class::getName
返回一个不是二进制名称的字符串,如前面所述。Class::getCanonicalName
返回null
,表示隐藏类没有规范名称。(请注意,Java 语言中匿名类的Class
对象具有相同的行为。)隐藏类中声明的最终字段是不可修改的。对隐藏类最终字段的
Field::set
和其他设置器方法将抛出IllegalAccessException
,无论字段的accessible
标志 如何。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
项的常量池条目来实例化隐藏类。其他指令,如 getstatic
、getfield
、putstatic
、putfield
、invokestatic
和 invokevirtual
,也可以通过相同的常量池条目访问隐藏类的成员。在隐藏类内部直接使用隐藏类很重要,因为它简化了语言运行时和框架生成隐藏类的过程。
隐藏类通常具有与普通类相同的反射能力。也就是说,隐藏类中的代码可以定义普通类和隐藏类,并可以通过它们的 Class
对象来操作普通类和隐藏类。隐藏类甚至可以充当查找类。也就是说,隐藏类中的代码可以获得它自身的查找对象,这有助于处理隐藏的嵌套类(见下文)。
栈跟踪中的隐藏类
默认情况下,隐藏类的方法不会在栈跟踪中显示。它们代表语言运行时的实现细节,并且从未期望对诊断应用程序问题的开发人员有用。然而,它们可以通过 -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames
选项包含在栈跟踪中。
有三种 API 可以将堆栈跟踪具体化:Throwable::getStackTrace
、Thread::getStackTrace
和 Java 9 中引入的新 API StackWalker
。对于 Throwable::getStackTrace
和 Thread::getStackTrace
API,默认情况下会省略隐藏类的堆栈帧;它们可以通过与上述堆栈跟踪相同的选项进行包含。对于 StackWalker
API,只有在设置了 SHOW_HIDDEN_FRAMES 选项时,JVM 实现才应包含隐藏类的堆栈帧。这允许堆栈跟踪过滤以 在开发人员诊断应用程序问题时省略不必要的信息。
访问控制嵌套中的隐藏类
通过 JEP 181 在 Java 11 中引入的 嵌套 是一组类,它们允许彼此访问私有成员,但不需要 Java 语言中嵌套类通常关联的任何后门可访问性扩展方法。该集合是静态定义的:一个类作为嵌套宿主,其类文件枚举了其他作为嵌套成员的类;反过来,嵌套成员在其类文件中指示哪个类托管了嵌套。虽然静态成员对于从 Java 源代码生成的类文件来说工作得很好,但对于语言运行时动态生成的类文件来说通常是不够的。为了帮助这样的运行时,并鼓励使用 Lookup::defineHiddenClass
而不是 Unsafe::defineAnonymousClass
,隐藏类可以在运行时加入嵌套;而普通类则不能。
通过将 NESTMATE
选项传递给 Lookup::defineHiddenClass
,可以将隐藏类创建为现有嵌套的一个成员。隐藏类加入的嵌套不是由 Lookup::defineHiddenClass
的参数确定的。相反,要加入的嵌套是从查找类推断出来的,即从最初获得查找对象的代码所在的类:隐藏类是查找类所在嵌套的一个成员(见下文)。
为了使 Lookup::defineHiddenClass
能够将隐藏类添加到嵌套中,查找对象必须具有适当的权限,即 PRIVATE
和 MODULE
访问权限。这些权限断言查找对象是由查找类获得的,目的是允许其他代码扩展嵌套。
JVM 不允许嵌套嵌套。无论嵌套成员资格是静态定义还是动态定义的,一个嵌套的成员都不能作为另一个嵌套的主机。
如果查找类是一个普通类,则其作为嵌套的成员可能是静态指示的(通过 NestHost
),或者如果查找类是一个隐藏类,则其可能是动态设置的。静态嵌套成员资格的验证是延迟的。对于语言运行时或框架库来说,能够向可能具有不良嵌套成员资格的查找类的嵌套中添加隐藏类是很重要的。例如,考虑在 Java 8 中引入的 LambdaMetaFactory 框架。当类 C
的源代码包含 lambda 表达式时,相应的 C.class
文件在运行时使用 LambdaMetaFactory
来定义一个隐藏类,该类包含 lambda 表达式的主体并实现所需的功能接口。C.class
可能具有不良的 NestHost
属性,但 C
的执行永远不会引用 NestHost
属性中命名的类 H
。由于 lambda 体可能访问 C
的 private
成员,因此隐藏类也需要能够访问它们;因此,LambdaMetaFactory
尝试将隐藏类定义为由 C
托管的嵌套的一个成员。
假设我们有一个查找类 C,并且使用 NESTMATE
选项调用 defineHiddenClass
来创建一个隐藏类并将其添加到 C 的嵌套中。隐藏类的嵌套宿主按以下方式确定:
- 如果 C 是一个普通类且没有
NestHost
属性,那么 C 就是它自己的宿主,同时也是隐藏类的嵌套宿主。 - 如果 C 是一个普通类,并且具有名为 H 的有效
NestHost
属性,那么 C 的嵌套宿主 H 也是隐藏类的嵌套宿主。在这种情况下,隐藏类被添加为 H 的嵌套的一个成员。 - 如果 C 是一个普通类,但具有无效的 NestHost 属性,那么 C 被用作隐藏类的嵌套宿主。
- 如果 C 是一个没有使用
NESTMATE
选项创建的隐藏类,那么 C 就是它自己的宿主,同时也是隐藏类的嵌套宿主。 - 如果 C 是一个使用
NESTMATE
选项创建的隐藏类,并且动态地添加到 D 的嵌套中,那么 D 的嵌套宿主被用作隐藏类的嵌套宿主。
如果隐藏类是在没有 NESTMATE
选项的情况下创建的,那么隐藏类就是它自己的嵌套的宿主。这与每个类要么是另一个类的嵌套宿主的一个嵌套成员,要么本身就是嵌套宿主的嵌套成员的策略保持一致。隐藏类可以创建额外的隐藏类作为其嵌套的成员:隐藏类中的代码首先在其自身上获取一个查找对象,然后在该对象上调用 Lookup::defineHiddenClass
并传递 NESTMATE
选项。
给定一个作为嵌套成员创建的隐藏类的 Class
对象,Class::getNestHost
和 Class::isNestmateOf
将按预期工作。可以在嵌套中的任何类的 Class
对象上调用 Class::getNestMembers
——无论是成员还是宿主,无论是普通类还是隐藏类——但仅返回静态定义的成员(即主机中由 NestMembers
枚举的普通类)以及嵌套宿主。
Class::getNestMembers
不包括动态添加到嵌套中的隐藏类,因为隐藏类是不可发现的,并且仅对创建它们的代码感兴趣,这些代码已经知道嵌套成员资格。这可以防止隐藏类通过嵌套成员资格泄露,如果打算保持其私有性的话。
卸载隐藏类
类加载器定义的类与该类加载器之间存在强烈关系。特别地,每个 Class
对象都有一个指向定义它的 ClassLoader
的引用(定义它的 ClassLoader)。这告诉 JVM 在解析类中的符号时要使用哪个加载器。这种关系的一个后果是,除非定义它的加载器可以被垃圾收集器回收(JLS 12.7),否则无法卸载一个普通类。能够回收定义加载器意味着没有加载器的活动引用,这反过来又意味着没有加载器定义的任何类的活动引用。(如果这些类是可到达的,它们会引用加载器。)这种广泛的不活跃性是卸载普通类时唯一安全的状态。
因此,为了最大化卸载普通类的机会,重要的是要尽量减少对该类及其定义加载器的引用。语言运行时通常通过创建许多类加载器来实现这一点,每个类加载器专门用于定义一个类,或者可能是少数几个相关的类。当类的所有实例都被回收时,假设运行时没有保留类加载器,那么该类及其定义加载器都可以被回收。然而,这会导致大量的类加载器,对内存的要求很高。此外,根据微基准测试,ClassLoader::defineClass
比 Unsafe::defineAnonymousClass
慢得多。
隐藏类不是由类加载器创建的,并且与其名义上的定义加载器之间只有松散的联系。我们可以利用这些事实,允许即使隐藏类的名义定义加载器无法被垃圾收集器回收,也可以卸载隐藏类。只要存在对隐藏类的活动引用(即对隐藏类的实例或对其 Class
对象的引用),那么隐藏类就会保持其名义定义加载器的活动状态,以便 JVM 可以使用该加载器来解析隐藏类中的符号。然而,当对隐藏类的最后一个活动引用消失时,加载器无需通过保持隐藏类的活动状态来回报。
在定义加载器可到达时卸载普通类是不安全的,因为 JVM 或使用反射的代码稍后可能会要求加载器重新加载该类,即加载具有相同名称的类。当静态初始化器第二次运行时,这可能会产生不可预测的效果。而对于卸载隐藏类则没有这样的担忧,因为隐藏类的创建方式与普通类不同。由于隐藏类的名称是 Lookup::defineHiddenClass
的输出,而不是输入,因此无法重新创建之前卸载的“相同”隐藏类。
默认情况下,Lookup::defineHiddenClass
会创建一个隐藏类,无论其名义定义加载器是否仍然存活,都可以卸载该隐藏类。也就是说,当隐藏类的所有实例都被回收且隐藏类不再可到达时,即使其名义定义加载器仍然可到达,该隐藏类也可能被卸载。当语言运行时创建隐藏类以服务于由任意类加载器定义的多个类时,这种行为很有用:相对于 ClassLoader::defineClass
和 Unsafe::defineAnonymousClass
,运行时将看到足迹和性能方面的改进。在其他情况下,语言运行时可能会将隐藏类链接到只有一个普通类,或者可能是少量具有与隐藏类相同定义加载器的普通类。在这种情况下,如果隐藏类必须与普通类共存,则可以将 STRONG
选项传递给 Lookup::defineHiddenClass
。这将安排隐藏类与其名义定义加载器之间具有与普通类与其定义加载器之间的相同强关系,也就是说,只有在可以回收其名义定义加载器时,才会卸载隐藏类。
备选方案
除了现有的为代理类生成包私有访问桥以访问目标类的私有成员的解决方法外,没有其他在运行时注入嵌套友元的方法。如果类对类加载器可见,则没有其他方法将其隐藏在其他类之外。
测试
我们将更新
LambdaMetaFactory
、StringConcatFactory
和LambdaForms
以使用新的 API。性能测试将确保 lambda 链接或字符串连接没有性能下降。将为新的 API 开发单元测试。
风险与假设
我们假设目前使用 Unsafe::defineAnonymousClass
的开发者能够轻松地迁移到 Lookup::defineHiddenClass
。开发者应该了解与 VM 匿名类相比,隐藏类在功能上的三个小约束。
- 受保护访问。 令人惊讶的是,VM 匿名类即使存在于不同的运行时包中且不是宿主类的子类,也可以访问宿主类的
protected
成员。相比之下,隐藏类会正确应用访问控制规则:隐藏类只能访问与其处于同一运行时包中或作为其他类子类的其他类的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 代码定义的隐藏类,也将无法控制自身的优化。(这被认为不会对 JDK 代码从定义 VM 匿名类迁移到定义隐藏类带来任何风险。)
作为相关事项,VM 匿名类可以使用 @Hidden
注解来防止其方法出现在堆栈跟踪中。当然,对于隐藏类来说,这种功能是自动的,并且未来可能会 提供给其他类。
迁移时需要考虑以下因素:
要从隐藏类中的代码调用私有嵌套伙伴实例方法,请使用
invokevirtual
或invokeinterface
代替invokespecial
。使用invokespecial
调用私有嵌套伙伴实例方法的生成字节码将无法通过验证。invokespecial
仅应用于调用私有嵌套伙伴构造函数。如前所述,对隐藏类的
Class
对象调用getName
方法将返回一个不是二进制名称的字符串,因为它包含/
字符。用户级代码不应与这样的Class
对象接触,但假设每个类都有二进制名称的框架级代码可能需要更新以处理隐藏类。之前已更新为处理 VM 匿名类的框架级代码将继续工作,因为隐藏类使用与 VM 匿名类相同的命名约定。JVM TI
GetClassSignature
返回 JNI 样式的签名,并返回一个在内部形式下不是二进制名称的字符串,例如包含.
字符的字符串。假设每个类都有二进制名称的 JVM TI 代理和工具可能需要更新以处理隐藏类。另一方面,JDI 实现已更新为处理隐藏类。JVM TI 代理无法修改隐藏类。受隐藏类类签名影响的工具应受到限制。
依赖项
JEP 181(基于嵌套的访问控制) 引入了基于嵌套的访问控制上下文,其中嵌套中的所有类和接口在嵌套伙伴之间共享私有访问权限。