Skip to content

JEP 309: Dynamic Class-File Constants | 动态类文件常量

摘要

扩展 Java 类文件格式,以支持新的常量池形式 CONSTANT_Dynamic。加载 CONSTANT_Dynamic 时,将创建委托给引导方法,就像链接 invokedynamic 调用站点将链接委托给引导方法一样。

目标

我们寻求减少创建新型可具体化类文件常量的成本和干扰,从而为语言设计师和编译器实现者提供更广泛的表达性和性能选项。我们通过创建一个新的常量池形式来实现这一点,该形式可以使用用户提供的行为(以带有静态参数的引导方法的形式)进行参数化。

我们还将调整 JVM 和引导方法之间的链接时握手,以便将 invokedynamic 使用的引导 API 也应用于动态常量。

基于 invokedynamic 的经验,我们将对 invokedynamic 和动态常量的引导握手进行调整,放宽对引导方法参数列表处理的某些限制。

这项工作需要对 JDK 库支持进行原型设计,以支持几种代表性常量类型的样本,特别是变量句柄 (JEP 193)。为了支持这种原型设计,这项工作将与常量表达式的基本语言支持工作 (JEP 303) 进行协调。

非目标

本 JEP 旨在支持常量池中的任意常量。尽管还有关于引导方法的其他用途的提案,比如方法配方,但本 JEP 只集中在一个用途上。

本 JEP 的成功并不依赖于 Java 语言或 Java 编译器后端的支持,尽管如果它得到编译器后端的支持,那么它更有可能成功。

尽管大型聚合常量是 Java 翻译策略的一个弱点,但在有更好的方法将它们封装为常量形式(如冻结数组或原始类型专用列表)之前,本 JEP 无法解决聚合常量的问题。

成功指标

作为基本要求,应该能够实际使用常量池形式来描述原始类镜像(int.class)、nullenum 常量以及大多数形式的 VarHandle,这些都可以通过 CONSTANT_Dynamic 来表示。

动态常量必须能够在当前允许使用一般常量池常量的任何上下文中使用,例如 CONSTANT_StringCONSTANT_MethodType。因此,它们必须是 ldc 指令的有效操作数,并且必须允许作为引导方法的静态参数。

引导方法握手应该支持包含数千个组件参数的复杂常量,从而取消当前 251 个常量参数的限制。作为一个扩展目标,还应该有办法让引导方法更准确地控制通过解析引导方法参数产生的链接错误。

在这项工作的最后,我们也应该有理由相信这种机制可以应用于多种库类型,如派生方法句柄、小型不可变集合(列表、映射、集合)、数值、正则表达式、字符串格式化程序或简单数据类。

应该确定并记录后续工作。请参阅下面的“可能的扩展”。

动机

Java 虚拟机规范的第 4.4 节描述了常量池的格式。添加新的常量池形式,如在 Java 7 中引入的 MethodHandleMethodType 支持,是一项重大的工作,并且会在整个生态系统中产生涟漪效应,因为它会影响所有解析或解释类文件的代码。这为创建新的常量池形式设置了非常高的门槛。

由于 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_MethodrefCONSTANT_Fieldref 常量用于引用类的命名成员,而类似的 CONSTANT_InvokeDynamicCONSTANT_Dynamic 常量则用于引用具有用户编程引导程序的命名实体。

对于 invokedynamicCONSTANT_Dynamic,常量的类型组件决定了调用点或常量的有效类型(分别)。引导方法不贡献或限制此类型信息,因此引导方法可以(而且通常是)弱类型,而字节码本身始终是强类型。

为了放宽对引导说明符的长度限制,将调整定义引导方法调用的语言(同时保持完全向后兼容性),以允许可变参数(ACC_VARARGS)引导方法将其尾随参数吸收为所有剩余的静态参数,即使这些参数有 2^16-1 个。(类文件格式已经允许这样做,尽管没有办法读取过长的引导参数列表。)为了保持一致性,如果目标方法有可变参数,MethodHandleinvokeWithArguments 方法也将以这种方式扩展。这样,引导方法的调用就可以使用弱类型方法 invokeWithArgumentsinvoke 来指定,就像今天只使用 invoke 来指定一样。

引导链接错误的控制已经证明是 invokedynamic 用户中反复出现的错误和 RFE(功能需求增强)的来源,并且随着引导方法变得更加复杂(随着动态常量的出现,这是必须的),这种趋势可能会加速。如果我们能找到一种方法,以更简单的方式为引导方法提供更全面的异常控制,我们会考虑将其作为本 JEP(Java 增强提案)的一部分提供。否则,它将列入未来的改进清单中。

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 映射表的翻译,如果可能的话,使用新的常量。