2026 年 7 月 31 日,包含 2934 次提交、改动 1888 个文件的 PR #31120 正式合并进 openjdk:master 分支。这一改动将 JEP 401(Value Objects 预览版)与 JEP 539(JVM 严格字段初始化预览版)一同合入主干,宣告 Project Valhalla 酝酿 12 年的「无身份对象」正式驶入 JDK 28 发布轨道。
2934 次提交背后的 12 年重构
合并进入主干的 PR #31120 在社区引发了 156 条核心讨论。从 2014 年 Oracle 正式启动 Project Valhalla 算起,这项工程经历了从 Value Types 到 Inline Classes、再到 Value Classes and Objects 的多次概念演进。1888 个文件的巨量修改证明无身份对象并非局部的语法拓展,而是贯穿编译器、虚拟机以及基础类库的全局重构。
在开发分支 valhalla/lworld 上,开发团队保持着每周与主干同步的频率,最终由 Coleen Phillimore、Ioi Lam、Maurizio Cimadamore、Jan Lahoda 等十余位 OpenJDK 核心 Reviewer 完成了审阅。为了控制代码审查的复杂度,整个变更被拆分为语言层 javac(JDK-8317277)、HotSpot 虚拟机层(JDK-8317278)和标准库层(JDK-8317279)三个子 PR 联动推进。这种分层拆解与高频同步的工程策略,保证了基础架构重大变更在维持主干稳定性的前提下顺利落地。
拆除 Identity: 内存布局从指针网到扁平数组
在传统的 Java 对象模型中,每个对象实例都带有固定的对象头(Object Header),用于支持锁状态、垃圾回收标记以及身份哈希码(Identity HashCode)。这种设计使得即便是一个仅包含 64 位整数的 Long 对象,也会产生高达 16 至 24 字节的内存开销与指针间接访问。JEP 401 引入了 value 关键字,允许开发者声明没有身份的类(如 value record Point(short x, short y, short z)),相同状态的实例在语义上完全等价且可互相替换。剥离对象身份打破了 Java 长期建立在引用比较上的隐式假设,换取了 JVM 在执行层面的直接优化空间。
当类被声明为 value 后,JVM 能够在堆内存和栈帧中直接对其进行内联扁平化(Flattening)存储。传统数组存储的是指向各个对象的引用指针,而在值对象数组中,元素直接以裸数据的形式连续排列在内存里;对于包含可空值(Nullable Value)的数组,JVM 通过额外的 NULL_MARKER 位图进行状态标记,避免了额外指针开销。内存布局从分散的指针网络转变为连续的字节块,大幅提升了 CPU L1/L2 缓存的命中率并消除了解引用延迟。
图:identity 对象堆分配(左)与 value 对象扁平化内存布局(右)对比。来源:Joe Mwangi, “First Look at Java Valhalla”
内存与性能实测: 2.2 倍密度与翻倍吞吐
官方在 inside.java 针对 26-jep401ea2 构建版本公布的基准测试数据给出了量化的性能证明。在对 5000 万个 LocalDate 实例构成的数组进行求和计算时,传统 Identity 版本的耗时为 82.7 毫秒,而迁移至 Value 版本后耗时下降至 41.9 毫秒。社区针对 1000 万个 PointRecord 数组的内存实验同样显示,传统 Identity 数组占用约 309 MB 堆内存,而 Value 数组仅占用约 138 MB。运算耗时缩减约 50% 且内存占用降低 55.3%,直接证明了连续内存布局在消除 GC 遍历开销与提升缓存局部性上的巨大威力。
图:10M 个 PointRecord 数组的堆内存占用对比。来源:Joe Mwangi, “First Look at Java Valhalla”
在硬件指令集加速方面,长时间等待 Valhalla 适用的 Vector API 也迎来了关键拼图。硬件级的 SIMD(单指令多数据流)向量计算要求数据在内存中必须具备紧凑连续的排列结构。在缺少值对象支持前,Vector API 必须依赖复杂的离线装箱解箱转换,限制了高吞吐数据流处理的执行效率。值对象的内联特性补齐了 Vector API 走向正式版的核心底座,为 Java 在高性能数值计算领域铺平了道路。
核心库迁移与生态涟漪: Integer 不再是传统对象
JEP 401 的合并不仅仅是向开发者提供一个可选的 value 关键字,JDK 基础类库本身已经全面开启了无身份化迁移。在最新的构建版本中,调用 Objects.hasIdentity(Integer.valueOf(123)) 或 Objects.hasIdentity(LocalDate.now()) 均返回 false;而调用 Objects.hasIdentity("abc") 与 Objects.hasIdentity(new ArrayList<>()) 则依然返回 true。核心包装类与时间类的身份剥离,意味着传统依靠 synchronized(integer) 这类把包装对象当锁的做法将不再可行——没有 identity,就没有可锁定的对象实体。
底层改变对上层生态库和框架提出了全新的兼容性要求。依赖 Java 序列化、Unsafe 物理地址直接修改字段、或者依赖对象身份进行 GC 追踪的传统框架,都需要调整内部实现机制。Hacker News 上 59 条讨论里,升级 JDK 28 时的行为变更与兼容性隐患是反复出现的话题。生态层面的升级适配是 Java 清理历史包袱、换取未来性能竞争力的必然过程。
面向对象与原生性能的终极收拢
自 1995 年诞生以来,Java 便处于面向对象抽象表达与原生类型性能优化之间的分离状态。为了保持面向对象的统一性,开发者不得不忍受自动装箱带来的 GC 压力与内存碎片。JEP 401 的落地证明了强类型系统可以同时兼容高阶类型封装与 C 语言级别的内存控制能力。无身份对象的引入使 Java 开发者摆脱了在代码可读性与机器执行效率之间的权衡纠结。
Project Valhalla 在 JDK 28 中的预览并不意味着演进的终点,后续关于 Generic Over Primitive(泛型直接特化原生类型)的探索将进一步放大值对象的价值。Java 语言从依赖堆内存分配与 GC 优化的传统平台,演变为兼具高级抽象与硬件亲和力的现代化运行环境。JEP 401 的核心意义在于重塑了 Java 的内存基石——当 12 年的架构设想真正转化为二进制代码时,Java 漫长的万物皆引用时代正式落下了帷幕。
参考链接: