JEP 309:动态类文件常量
概述
扩展 Java 类文件格式以支持一种新的常量池形式 CONSTANT_Dynamic
。加载 CONSTANT_Dynamic
时,会将其创建委托给一个引导方法(bootstrap method),这与链接 invokedynamic
调用点时将链接委托给引导方法的方式类似。
目标
我们力求降低创建新型可物化类文件常量的成本和干扰,这反过来为语言设计者和编译器实现者提供了更广泛的表达能力和性能选项。我们通过创建一种新的常量池形式来实现这一点,这种形式可以通过用户提供的行为(以带有静态参数的引导方法的形式)进行参数化。
我们还将调整 JVM 与引导方法之间的链接时握手,从而适配 invokedynamic
使用的引导 API,使其同样适用于动态常量。
基于 invokedynamic
的经验,我们将对 invokedynamic
和动态常量的引导握手进行调整,放宽对引导方法参数列表处理的某些限制。
非目标
本 JEP 旨在支持常量池中的任意常量。尽管还有其他关于引导方法的使用建议,例如方法配方,但本 JEP 专注于其中一种用途。
此 JEP 的成功并不取决于 Java 语言或 Java 编译器后端的支持,但如果编译器后端使用它,则更有可能成功。
尽管大型聚合常量是 Java 转换策略中的一个薄弱环节,但在有更好的方法将它们封装为常量形式(例如冻结数组或针对基本类型的专用列表)之前,这个 JEP 无法解决聚合问题。
成功指标
至少应该可以方便地以 CONSTANT_Dynamic
的形式,将常量池形式公开用于描述原始类镜像(int.class
)、null
、enum
常量以及大多数形式的 VarHandle
。
动态常量必须能够在任何目前允许通用常量池常量的上下文中使用,例如 CONSTANT_String
和 CONSTANT_MethodType
。因此,它们必须是 ldc
指令的有效操作数,并且必须允许作为引导方法的静态参数。
引导方法握手(bootstrap-method handshake)应该支持包含数千个组件参数的复杂常量,从而突破当前 251 个常量参数的限制。作为扩展目标,还应该有一种方法可以让引导方法更精确地控制在解析引导方法参数时产生的链接错误。
在工作结束时,我们也有理由相信,该机制可以适用于各种各样的库类型,例如派生的方法句柄、小型不可变集合(列表、映射、集合)、数值、正则表达式、字符串格式化程序或简单的数据类。
应确定并记录后续工作。请参见下面的“可能的扩展”。
动机
《Java 虚拟机规范》的 4.4 节描述了常量池的格式。添加新的常量池形式(例如 Java 7 中引入的对 MethodHandle
和 MethodType
的支持)是一项重大的工作,且会在整个生态系统中产生连锁反应,因为它会影响所有解析或解释类文件的代码。这就为创建新的常量池形式设置了非常高的门槛。
通过 invokedynamic
,将复杂数据存储在常量池中的价值倍增,因为 invokedynamic
引导程序的静态参数列表是一系列常量。invokedynamic
协议的设计者(例如 Java 8 中添加的 LambdaMetafactory
)通常需要努力将行为编码为现有常量集的形式——这反过来又要求引导程序本身包含额外的、容易出错的验证和提取逻辑。更丰富、更灵活、更高类型化的常量减少了开发 invokedynamic
协议的阻力,从而促进了复杂逻辑从运行时向链接时的转移,提高了程序性能并简化了编译器逻辑。
描述
正如 invokedynamic
调用点的链接涉及从 JVM 到基于 Java 的链接逻辑的上行调用一样,我们可以将这一技巧同样应用于常量池条目的解析。一个 CONSTANT_Dynamic
常量池条目编码了用于解析的引导方法(一个 MethodHandle
)、常量的类型(一个 Class
),以及任何静态引导参数(一个任意的常量序列,前提是常量池中动态常量之间不能存在循环)。
我们添加了一种新的常量池形式,CONSTANT_Dynamic
(新的常量标签 17),其标签字节后包含两个组成部分:引导方法的索引,格式与 CONSTANT_InvokeDynamic
中找到的索引相同,以及一个 CONSTANT_NameAndType
,用于编码期望的类型。
行为上,CONSTANT_Dynamic
常量通过在其引导方法中执行以下参数来解析:1. 一个本地的 Lookup
对象,2. 表示常量名称部分的 String
,3. 表示预期常量类型的 Class
,以及 4. 任何剩余的引导参数。与 invokedynamic
类似,多个线程可以竞争进行解析,但会选出唯一的胜者,并丢弃其他竞争答案。不同于 invokedynamic
指令要求返回一个 CallSite
对象,引导方法将返回一个值,该值会立即转换为所需的类型。
与 invokedynamic
类似,名称组件是除类型之外的另一个通道,用于将表达式信息传递给引导方法。可以预见,正如 invokedynamic
指令对名称组件(例如,方法名或某些特别描述符)有所用途一样,动态常量也会对名称(例如,enum
常量的名称或符号常量的拼写)有所用途。在两个地方都放置 CONSTANT_NameAndType
会使设计更加规则。实际上,CONSTANT_Methodref
和 CONSTANT_Fieldref
常量被用来引用类的命名成员,而类似的 CONSTANT_InvokeDynamic
和 CONSTANT_Dynamic
常量则被用来引用具有用户编程引导的命名实体。
常量的类型组件(对于 invokedynamic
和 CONSTANT_Dynamic
来说)分别决定了调用站点或常量的有效类型。引导方法并不提供或限制此类型信息,因此引导方法可能是(并且通常是)弱类型的,而字节码本身始终是强类型的。
为了放宽对引导程序说明符的长度限制,定义引导方法调用的语言将进行调整(完全向后兼容),以允许变长参数(ACC_VARARGS
)引导方法将其剩余的所有静态参数吸收进其尾随参数中,即使有 2^16-1 个参数也是如此。(类文件格式已经允许这样做,尽管没有办法读取超长的引导参数列表。)为了保持一致性,如果目标方法具有可变参数,则 MethodHandle
的 invokeWithArguments
方法也会以这种方式扩展。通过这种方式,引导方法的调用可以依据弱类型方法 invokeWithArguments
和 invoke
来指定,正如今天仅使用 invoke
指定一样。
控制引导链接错误已被证明是 invokedynamic
用户反馈的 bug 和 RFE(功能增强请求)的反复来源,而且随着引导方法变得更加复杂(这是必然趋势,因为动态常量需要更复杂的引导方法),这一趋势可能会加速。如果我们能找到一种方法,为引导方法提供对异常的更全面控制,并且实现起来比较简单,我们将考虑将其作为此 JEP 的一部分交付。否则,它将被列入未来增强功能的清单中。
CONSTANT_Dynamic
的 Java 虚拟机规范草案可以在 JDK-8189199 中找到,这是与本 JEP 的主要开发问题相关的 CSR 问题。
未来工作
可能的未来扩展包括:
- 支持大规模常量,例如数组或资源表
- 对引导方法握手的进一步调整
- 可能与动态常量产生协同效应的其他引导方法用途
- 将动态常量附加到静态字段的
ConstantValue
属性 - 在 Java 语言中实现常量的惰性初始化
- 将新常量与 Java 语言中用于常量表达式的特殊规则集成
有关设计选择的讨论可以在 JDK-8161256 中找到,其中涉及多个相关的 RFE(请求增强功能)。本 JEP 是从这一更大的功能列表中提炼出来的。
替代方案
CONSTANT_Dynamic
的许多用法可以替换为等效的 invokedynamic
调用。(该调用将不带任何参数,并绑定到返回所需常量的方法句柄。)然而,这种解决方法并不能满足一个关键需求,即能够将合成常量作为引导参数传递。
CONSTANT_Dynamic
的另一种替代方法是使用 static final
字段为所需的常量命名,并在静态初始化器(<clinit>
)中计算它们的值。这种方法需要额外的元数据(每个常量都需要一个临时字段定义),并且不够惰性,无法避免引导循环问题。通常通过构建具有解耦静态初始化器的私有嵌套类来解决这些问题,但这也需要额外的元数据。如果语言发展到使用大量此类常量,过多的元数据将会导致应用程序膨胀。
另一种方法是旋转执行常量细化逻辑的静态方法,然后从 invokedynamic
中惰性调用它们。同样,这种一次性使用的方法是一种元数据开销,与 CONSTANT_Dynamic
相比,其开销较大。
在实践中,模拟这些特性的元数据开销太大。
依赖
此功能以 JVM 为中心,因此不依赖于较高的软件层。
为了确保设计正确,它要求至少被几个用例进行试验性采用。即使原型会被丢弃,库的原型设计也是必须的。
与 invokedynamic
一样,广泛的采用需要 javac
后端的支持,而这可能又需要语言的扩展。作为基本的第一步,应该检查那些需要隐藏静态方法的转换变通方案(例如 int.class
或 switch 映射表的转换),并在可能的情况下使用新的常量重新表述。