跳到主要内容

JEP 371:隐藏类

概括

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

目标

  • 允许框架将类定义为框架的不可发现的实现细节,以便它们不能被其他类链接,也不能通过反射发现。

  • 支持使用不可发现的类扩展访问控制嵌套。

  • 支持主动卸载不可发现的类,以便框架可以灵活地定义所需的数量。

  • 弃用非标准 API sun.misc.Unsafe::defineAnonymousClass,目的是在未来版本中弃用并删除它。

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

非目标

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

动机

许多基于 JVM 构建的语言实现都依赖动态类生成来实现灵活性和效率。例如,在 Java 语言的情况下,javac不会class在编译时将 lambda 表达式翻译成专用文件,而是发出动态生成并实例化类的字节码,以在需要时生成与 lambda 表达式相对应的对象。类似地,非 Java 语言的运行时通常通过使用动态代理来实现这些语言的高阶功能,动态代理也会动态生成类。

语言实现者通常希望动态生成的类在逻辑上成为静态生成的类的实现的一部分。此意图建议动态生成的类所需的各种属性:

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

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

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

不幸的是,定义类的标准 APIClassLoader::defineClassLookup::defineClass类的字节码是动态(在运行时)还是静态(在编译时)生成无关。这些 API 总是定义一个_可见的_类,每次同一加载器层次结构中的另一个类尝试链接该名称的类时都会使用该类。因此,该类可能比预期更容易被发现或具有更长的生命周期。此外,如果嵌套的宿主类事先知道成员类的名称,API 只能定义一个充当嵌套成员的类;实际上,这可以防止动态生成的类成为嵌套的成员。

如果标准 API 可以定义不可发现且生命周期有限的_隐藏_类,那么动态生成类的 JDK 内部和外部的框架都可以定义隐藏类。这将提高基于 JVM 构建的所有语言实现的效率。例如:

  • java.lang.reflect.Proxy可以定义隐藏类作为代理类,实现代理接口;

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

  • java.lang.invoke.LambdaMetaFactory可以生成隐藏的嵌套类来保存访问封闭变量的 lambda 体;和

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

描述

Java 7 中引入的APILookup允许类获取_查找对象_,该对象提供对类、方法和字段的反射访问。至关重要的是,无论什么代码最终使用查找对象,反射访问始终发生在最初获取查找对象的类(查找类)的上下文中。实际上,查找对象将查找类的访问权限传输给接收该对象的任何代码。

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

此 JEP 建议扩展LookupAPI 以支持定义只能通过反射访问的_隐藏类_。在字节码链接期间,JVM 无法发现隐藏类,显式使用类加载器的程序也无法发现隐藏类(例如,通过_、_和Class::forNameClassLoader::loadClass。当隐藏类不再可访问时,可以将其卸载,或者可以共享类加载器的生命周期,以便仅在类加载器被垃圾回收时才卸载它。或者,可以将隐藏类创建为访问控制嵌套的成员。

为了简洁起见,这个 JEP 谈到了“隐藏类”,但应该将其理解为隐藏类或接口。同样,“普通类”表示普通类或接口,是ClassLoader::defineClass.

创建隐藏类

普通类是通过调用创建的ClassLoader::defineClass,而隐藏类是通过调用创建的Lookup::defineHiddenClass。这会导致 JVM 从提供的字节派生隐藏类,链接隐藏类,并返回提供对隐藏类的反射访问的查找对象。调用程序应该仔细存储查找对象,因为这是获取Class隐藏类对象的唯一方法。

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

创建隐藏类的主要区别在于它所指定的名称。**隐藏类不是匿名的。**它有一个可通过访问的名称,Class::getName并且可以在诊断(例如 的输出java -verbose:class)、JVM TI 类加载事件、JFR 事件和堆栈跟踪中显示。然而,该名称的形式非常不寻常,它实际上使该类对所有其他类不可见。该名称是以下内容的串联:

  1. this_class结构中指定的内部形式 (JVMS 4.2.1) 的二进制名称ClassFile,例如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.x,其结果Class::getName是以下各项的串联:

  1. 二进制名称A.B.C(通过将每个名称A/B/C替换为 来获得);'/'``'.'
  2. 人物;和
  3. 不合格的名字x

例如,如果隐藏类名为com/example/Foo.1234,则 的结果Class::getNamecom.example.Foo/1234。同样,该字符串既不是二进制名称,也不是二进制名称的内部形式。

隐藏类的命名空间与普通类的命名空间不相交。给定一个ClassFile结构 wherethis_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 从未说过隐藏类已“加载”。**没有类加载器被记录为隐藏类的启动加载器,并且不会生成涉及隐藏类的加载约束。因此,任何类加载器都不知道隐藏类:类的运行时常量池中对由 表示的D类的符号引用永远不会解析为任何值、和的隐藏类。反射方法、、 和不会找到隐藏类。C``N``D``C``N``Class::forName``ClassLoader::findLoadedClass``Lookup::findClass

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

使用隐藏类

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

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

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

  3. 隐藏类中声明的 Final 字段不可修改。Field::set隐藏类的 Final 字段上的其他 setter 方法将抛出异常,IllegalAccessException无论该字段的accessible flag是什么。

  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隐藏类中的指令可以通过常量池条目实例化隐藏类,该常量池this_class条目直接引用当前ClassFile.其他指令,例如getstaticgetfieldputstaticputfieldinvokestaticinvokevirtual,可以通过相同的常量池条目访问隐藏类的成员。在隐藏类内部直接使用很重要,因为它简化了语言运行时和框架生成隐藏类的过程。

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

堆栈跟踪中的隐藏类

默认情况下,隐藏类的方法不会显示在堆栈跟踪中。它们代表语言运行时的实现细节,并且永远不会对开发人员诊断应用程序问题有用。但是,它们可以通过选项包含在堆栈跟踪中-XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames

共有三个 API 可以具体化堆栈跟踪:Throwable::getStackTraceThread::getStackTraceJava StackWalker9 中引入的较新 API。对于Throwable::getStackTraceThread::getStackTraceAPI,默认情况下会忽略隐藏类的堆栈帧;它们可以包含在与上面的堆栈跟踪相同的选项中。对于API,仅当设置了SHOW_HIDDEN_FRAMESStackWalker选项时,JVM 实现才应包含隐藏类的堆栈帧。这允许堆栈跟踪过滤在开发人员诊断应用程序问题时省略不必要的信息

访问控制嵌套中的隐藏类

嵌套由JEP 181在 Java 11 中引入,是一组_类_,允许访问彼此的私有成员,但没有任何通常与 Java 语言中的嵌套类关联的后门可访问性扩展方法。该集合是静态定义的:一个类充当嵌套宿主,其类文件枚举作为嵌套成员的其他类;反过来,嵌套成员在其类文件中指示哪个类托管嵌套。虽然静态成员资格对于从 Java 源代码生成的类文件来说效果很好,但对于语言运行时动态生成的类文件来说通常是不够的。为了帮助此类运行时,并鼓励使用Lookup::defineHiddenClassover 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类。由于 lambda 主体可以访问的成员,因此隐藏类也需要能够访问它们;因此,尝试将隐藏类定义为 托管的嵌套的成员。H``NestHost``private``C``LambdaMetaFactory``C

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

  • 如果C是一个普通类并且缺少NestHost属性,那么C就是它自己的宿主,也是隐藏类的嵌套宿主。
  • 如果 C 是一个普通类,具有NestHost名为 H 的有效属性,则 C 的嵌套宿主 H 是隐藏类的嵌套宿主。在这种情况下,隐藏类被添加为 H 嵌套的成员。
  • 如果 C 是一个具有错误 NestHost 属性的普通类,则 C 将用作隐藏类的嵌套宿主。
  • 如果C是一个不NESTMATE带选项创建的隐藏类,那么C就是它自己的宿主,也是隐藏类的嵌套宿主。
  • 如果 C 是使用 option 创建的隐藏类NESTMATE并动态添加到 D 的嵌套中,则 D 的嵌套主机将用作隐藏类的嵌套主机。

如果创建隐藏类时没有该NESTMATE选项,则该隐藏类是其自己的嵌套的宿主。这符合以下策略:每个类要么是嵌套的成员,而另一个类作为嵌套宿主,要么本身就是嵌套的嵌套宿主。隐藏类可以创建其他隐藏类作为其嵌套的成员:隐藏类中的代码首先获取自身的查找对象,然后调用Lookup::defineHiddenClass该对象并传递NESTMATE选项。

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

Class::getNestMembers不包括动态添加到嵌套中的隐藏类,因为隐藏类是不可发现的,并且应该只对创建它们的代码感兴趣,这些代码已经知道嵌套成员资格。如果打算保持私有,这可以防止隐藏类通过嵌套成员身份泄漏。

卸载隐藏类

类加载器定义的类与该类加载器有很强的关系。特别是,每个Class对象都有一个对ClassLoader 定义它的对象的引用。这告诉 JVM 在解析类中的符号时使用哪个加载器。这种关系的一个结果是,除非垃圾收集器可以回收其定义的加载器,否则无法卸载普通类(JLS 12.7)。能够回收定义的加载器意味着不存在对加载器的实时引用,这又意味着不存在对加载器定义的任何类的实时引用。 (这样的类,如果它们是可访问的,将引用加载器。)这种普遍缺乏活动性是唯一可以安全地卸载普通类的状态。

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

隐藏类不是由类加载器创建的,并且仅与被视为其定义加载器的类加载器有松散的连接。我们可以通过允许卸载隐藏类来将这些事实转化为我们的优势,即使其名义定义加载器无法被垃圾收集器回收。只要存在对隐藏类的实时引用(无论是隐藏类的实例还是其对象Class),隐藏类就会保持其名义定义加载器处于活动状态,以便 JVM 可以使用该加载器来解析隐藏类。然而,当对隐藏类的最后一个实时引用消失时,加载器不需要通过保持隐藏类处于活动状态来回报。

在其定义加载器可到达时卸载普通类是不安全的,因为加载器稍后可能会被 JVM 或使用反射的代码要求重新加载该类,即加载具有相同名称的类。当静态初始化程序第二次运行时,这可能会产生不可预测的影响。卸载隐藏类并不存在这样的问题,因为隐藏类不是以相同的方式创建的。由于隐藏类的名称是 的输出Lookup::defineHiddenClass,而不是输入,因此无法重新创建之前卸载的“相同”隐藏类。

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

备择方案

除了为代理类生成包私有访问桥来访问目标类的私有成员的现有解决方法之外,没有其他选择可以在运行时注入嵌套对象。如果某个类对类加载器可见,则没有其他方法可以对其他类隐藏该类。

测试

  • 我们将更新LambdaMetaFactoryStringConcatFactory、 和LambdaForms以使用新的 API。性能测试将确保 lambda 链接或字符串连接不会出现回归。

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

风险和假设

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

  • 受保护的访问。protected令人惊讶的是,即使 VM 匿名类存在于不同的运行时包中并且不是主机类的子类,VM 匿名类也可以访问其主机类的成员。相反,访问控制规则适用于隐藏类:protected如果隐藏类与另一个类位于同一运行时包中,或者是另一个类的子类,则该隐藏类只能访问另一个类的成员。隐藏类对protected查找类的成员没有特殊的访问权限。

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

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

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

迁移应考虑以下因素:

  • 要从隐藏类中的代码调用私有 Nestmate 实例方法,请使用invokevirtualinvokeinterface代替invokespecial。生成的用于invokespecial调用私有 Nestmate 实例方法的字节码将无法通过验证。invokespecial只能用于调用私有嵌套构造函数。

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

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

依赖关系

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