Project Valhalla 与 JDK 28:将值类引入 Java

Project Valhalla 正在通过 JEP 401:值类与对象 在 JDK 28 中到来。此更新允许开发者定义“代码像类但行为像 int”的类,使 JVM 能够在内存中密集存储数据,并降低与对象标识、指针和对象头相关的开销。

核心问题:“松散”内存布局

Java 的根本设计——几乎所有东西都是引用类型——导致了“松散”的内存布局。当程序员创建对象数组时,JVM 并不会将对象连续存储,而是存储指向分散在堆中的对象的指针(引用)数组。

此架构导致两个主要的性能瓶颈:

  1. 指针间接: 每次访问对象字段都需要通过指针进行一次“跳转”,增加缓存未命中的风险。
  2. 对象开销: 堆上的每个对象都携带一个头部(类型和同步的元数据),占用大量内存。

现代 CPU 的速度比主内存快几个数量级,使 引用局部性 成为关键。当数据密集存储时,一个 CPU 缓存行(通常为 64 字节)可以一次加载多个值。当前以引用为主的模型常常迫使 CPU 等待内存读取,严重限制了数据密集型应用的性能。

JEP 401:值类与值对象

JDK 28 引入了 value 修饰符,允许创建 值类。这些类的实例是 值对象,它们由状态而非唯一标识来定义。

值类的关键特性

  • 无标识: 两个具有相同字段值的值对象被视为可替换的。它们没有唯一的内存地址来定义。
  • 隐式 final: 值类中的所有实例字段默认是 final。
  • 不可同步: 由于缺乏标识,不能在值对象上进行同步;尝试这样做会抛出 IdentityException
  • 仍为引用类型: 在 JDK 28 中,值类仍然是引用类型,这意味着它们 仍然可以为 null。非空值类型计划在未来的 JEP 中实现。

相等性的变化

value 对象的 == 运算符含义发生了变化。它不再比较内存地址(标识),而是检查 可替换性——即两个值是否属于同一类且具有相同的字段值。对于业务逻辑的相等性,仍建议使用 equals() 方法。

性能机制:标量化与扁平化

通过去除对象标识的需求,JVM 可以使用两种主要技术来优化值对象:

1. 标量化

此 JIT 编译器优化会将值对象“拆解”为其组成字段。JVM 不再传递指向对象的指针,而是直接在 CPU 寄存器中传递原始字段(例如 Color 对象的三个字节)。这实际上使对象分配“免费”,并减轻了垃圾收集器(GC)的负担。

2. 堆扁平化

JVM 可以将值对象的数据直接编码到字段或数组单元中,而无需指针。这产生了密集的内存布局,值对象数组被存储为连续的数据块,类似于原始数组(例如 int[])。

原子性约束: 为了进行扁平化,数据通常必须能够原子地读写,以防止并发访问时出现“撕裂”。在当前平台上,这通常将扁平化限制在能够在 64 位(包括空标志)内容纳的对象。更大的对象可能仍然以普通对象形式留在堆上,除非未来的更新(如 128 位编码或空限制类型)实现。

通向特化泛型的道路

虽然 JDK 28 引入了值类,但尚未解决泛型中的 类型擦除 问题。目前,List<Point> 仍然存储指向 Point 对象的引用,因为类型参数 T 在运行时会擦除为 Object,迫使值对象在堆上实例化。

Project Valhalla 的长期计划包括两个阶段来解决此问题:

  • 通用泛型: 语言层面的变更,使类型变量能够覆盖值类型(引入新的编译器警告以准备 API 的特化)。
  • 特化泛型: 未来的 JVM 扩展,将为具体类型参数生成特化的类布局,最终使 ArrayList<Point> 的效率可媲美 Point[]

JDK 28 交付概览

特性 JDK 28 中的状态 备注
value modifier 预览 允许声明值类/记录
标量化 预览 JIT 优化以消除分配
堆扁平化 预览 小型值对象的密集存储
装箱原始类型 预览 IntegerLong 等将成为值类
非空类型 未包含 计划在未来的 JEP 中实现
特化泛型 未包含 计划在未来的发行版中实现

社区观点与技术权衡

围绕本次发布的技术讨论突出了几个关键点:

"值类的 == 基本上就像 memcmp()。这有点不幸,因为它破坏了封装,暴露了实现细节。" — @layer8

"如果我有一个函数,其值 x 会擦除为 java.lang.Object……以前检查是否为 null 然后对该对象进行同步是安全的。现在这不再安全:这可能会直接抛出 IdentityException。" — @leiroigh

批评者指出,原子扁平化的 64 位限制可能将收益限制在非常小的数据类型上。其他人则认为,在第一阶段将值类保持为可空引用类型的决定简化了用户模型,但相较于严格的“原始类”模型降低了性能上限。然而,支持者认为,这种渐进式方法是维护 Java 庞大现有生态系统和向后兼容性的必要之举。

Sources