Project Valhalla 解读:十年耕耘终入 JDK 28

Hacker News Top 新闻

摘要

Project Valhalla 的 JEP 401(值类与对象)已合并到 OpenJDK 主仓库,目标为 JDK 28,标志着十年开发历程的重大里程碑。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/06/20 14:25

# Project Valhalla 详解:十年心血如何在 JDK 28 中落地 - JVM Weekly 第 180 期 来源:https://www.jvm-weekly.com/p/project-valhalla-explained-how-a 文章封面图片 (https://substackcdn.com/image/fetch/$s_!3GhD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d9124f8-8a73-453b-88e2-7df5c98022ec_1920x1080.png) 6 月 15 日,Oracle 工程师 [**Lois Foltan**](https://www.linkedin.com/in/ACoAAANN9nEBZCNNNQaQRKbqYcemLezVIRBcmYk?miniProfileUrn=urn%3Ali%3Afs_miniProfile%3AACoAAANN9nEBZCNNNQaQRKbqYcemLezVIRBcmYk) [确认](https://mail.openjdk.org/archives/list/[email protected]/message/AIA3O3LHFZ6T7TIPH7KZT4WS4B6U72U5/)了业界大部分早已不再相信的事情:[**JEP 401:值类与对象**](https://openjdk.org/jeps/401) 将被集成到 OpenJDK 主仓库,目标锁定 JDK 28。 [](https://substackcdn.com/image/fetch/$s_!Nxmc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0333ea9a-5db7-4b0b-9dfe-ae446b56b623_737x374.png) 这一变更规模巨大,剩余提交者被要求在集成期间暂停更大的提交。仅这一个[**拉取请求**](https://github.com/openjdk/jdk/pull/31120)就贡献了超过 19.7 万行代码,涉及 1,816 个文件。 [](https://substackcdn.com/image/fetch/$s_!TXj2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffb8578c0-ca06-4821-b945-ea9609626526_725x172.png) 不过,先别急着开香槟:这只是一个**预览版**,默认关闭,而且正如 Brian Goetz 迅速降温所言:“这只是 Valhalla 的第一部分。” Goetz 还补充了一个精彩观察:那些“他们永远发不出来”的人群,现在会顺利切换到“但他们没发布最重要的部分”(而且社区里多年来一直有个笑话:我们本人可能先到英灵殿(北欧神话中的死后之地),而非项目发布)。 *你得自己赢得那些黑粉。* [](https://substackcdn.com/image/fetch/$s_!T4O3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd61784ec-6d2a-43ea-8b7a-ca0d9ee7d512_640x262.gif) 所以现在正是讲述整个故事的绝佳时机。本期是一次深度解析,假设你**从未关注过 Valhalla 的工作进展**:从 2014 年的问题,到各种想法的演变(其中相当一部分最终进了垃圾桶),一直到 JDK 28 中我们将具体获得什么。给自己冲杯咖啡。这一期我酝酿已久,就等这个机会了。 [](https://substackcdn.com/image/fetch/$s_!BvJy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb5f166e-2c27-4ead-94f3-cf49cf09191a_1314x638.png) Valhalla 从始至终的口号是:“写起来像类,用起来像 int。” 这一句话就概括了整个项目的要点:我们希望编写正常、可读的类,包含方法、构造函数验证和合理的字段名,但 JVM 能够像对待基本类型那样高效地处理它们。 要理解为什么这是一个问题,你得回到 Java 的基础。在这个语言中,除了八种基本类型(int、long、double、boolean 等)之外,**一切都是引用类型**。当你写 `Point p = new Point(1, 2)` 时,变量 p 并不是**一个**点。变量 p 是一个指针,一个衣帽寄存牌:堆上某个位置有一个对象,你手里拿着一张写有它地址的纸条。每次你想读取一个字段,JVM 就必须“去衣帽间”,通过指针进行一次跳转(*指针间接引用*)。对于单个对象来说,这没什么。问题从规模开始出现。 堆上的每个对象都有自己的**头部**(十几字节的元数据:其中包括让 JVM 知道它是什么类型、是否有人在同步它等信息)。顺便说一句,这正是 **Project Lilliput** 最近一直在解决的问题,帮助缩小对象头部大小。但头部大小并非全部。每个对象都必须**分配**,之后还要**垃圾回收**。而且由于对象分散在堆上,一个包含一百万个点的数组,实际上就是一百万张指向散落在仓库里的百万个盒子的纸条。 Brian Goetz 在他的 **《State of Valhalla》文档**中,将这种内存布局称为*“fluffy”*:膨胀、臃肿。我们梦想的是*紧凑*布局,数据一个挨着一个。 [](https://substackcdn.com/image/fetch/$s_!6-Rq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb98703ea-c366-439e-8e2e-6cbddd74b4ab_1525x1004.png) 为什么紧凑性很重要?因为**硬件变化比 Java 更快**。1995 年,一次内存访问的成本大致相当于一次 CPU 操作。如今,CPU 比主存快两个数量级,整个差距由缓存来弥补。处理器以称为*缓存行*(通常 64 字节)的块来读取内存。如果数据密集且有序,一个这样的块就能一次读取大量有用值。如果我们通过指针跳转,每次访问都有*缓存未命中*的风险,这比命中可能慢上百倍。这就是*引用局部性*,也是整个游戏中的真正赌注。 “但是 JVM 有逃逸分析,”某个敏锐的人会说。没错:虚拟机可以识别出某些对象从未“逃逸”出代码的局部片段,于是根本不会分配它。从程序员的角度看,对象仿佛存在,但实际上它的字段被分散到普通变量或 CPU 寄存器中。在最佳情况下,分配以及后续垃圾回收的开销几乎降为零。 问题在于这种优化不可预测且脆弱。它只有在 JIT 编译器能够高度自信地追踪对象的整个生命周期时才有效。但只需对象落入另一个类的字段、存入数组、传入更复杂的方法、或出现在 JIT 可分析代码边界之外,整个技巧就会失效。源代码保持不变,但性能表现可能发生剧烈变化。这正是经验丰富的 JVM 程序员将逃逸分析视为一个不错的额外收益而非项目基础的原因。 如果一个应用的性能取决于特定 JIT 版本是否成功应用了这种优化,就很容易陷入难以预测的性能回退陷阱。一次微小的重构、一次 JDK 更新、或代码结构的变化,都可能将对象送回堆上,分配和垃圾回收的开销重回巅峰。 这就剩下蛮力选项:放弃对象,手动编码数据。不用 Color 类,而是保存三个字节 r、g、b。这不仅仅是学术示例。这种方法多年来一直用于游戏引擎、图形库、图像处理系统、数据库、分析引擎和 HPC 代码,在这些地方,每一字节内存和每一次分配都很重要。 问题在于,速度是以安全性和可读性为代价的。我们失去了名称、私有状态、验证和方法。JEP 401 给出了一个简单例子:处理“原始”颜色字节的开发人员可能错误地将它们解释为 BGR 而非 RGB,交换红色和蓝色,悄悄地破坏整个图像。类不会允许这种情况。一个裸 int?当然可以。 正是这种二分法——*要么方便的类,要么快速的基本类型*——正是 Valhalla 试图消除的。 官方上,**Project Valhalla 始于 2014 年**。[**James Gosling**](https://www.linkedin.com/in/jamesgosling?miniProfileUrn=urn%3Ali%3Afs_miniProfile%3AACoAAAAjOR4BALD_ZR564BLckv1R3tVohVN_Bm0) 当时将其描述为“六个博士学位打成一个结”,这绝非夸张。有趣的是,这个想法比项目本身更古老:Java 的创建者在语言的第一版就想引入值类型,但在 1995 年他们放弃了,因为问题太难。 目标设定得很宏伟:恢复**编程模型与现代硬件性能特性之间的一致性**。换句话说,让程序员能够声明自己的类型,这些类型在内存中像基本类型一样扁平、紧凑,但看起来和行为都像普通类。说起来容易做起来难。 在接下来的几年里,团队构建了**五个不同的原型**,每个都探索了问题的不同方面。而这也是故事中最有趣的部分开始的地方,因为要理解 Valhalla 当前形态,你必须看到有多少想法在途中消亡了。 早期原型朝着我们现在称为 **“Q 世界”** 的方向发展。它假设新的值类型与对象完全不同,具有单独的类型描述符、单独的字节码和单独的顶级类型,就像基本类型一样。听起来合理:如果它们应该像 int 一样工作,就让它们像 int 一样表示。问题是,这种分离给整个 JVM 类型系统注入了额外的复杂性:每件事都要做两套版本。 突破出现在一个被称为 **“L 世界”** 的原型(大致在 2019 年左右)。这个名字源于值类型开始与对象引用**共享同一个 “L 载体”**(L 描述符,JVM 用于普通引用的那个)。团队原本预计这样的统一会太难,然而,令他们自己惊讶的是,*它没有重大妥协就成功了*,并且顺便解决了早期回合中的一堆问题。 L 世界还产生了一个基本的“啊哈”时刻,塑造了后来的一切:**语言模型和 JVM 模型不必百分之百重叠**。L 世界是适合虚拟机的模型,但你可以将其视为*翻译目标*,并在语言中给程序员提供更方便的东西。这种层次分离后来成为项目其余部分的关键。 也是在那个时候,将工作分为**两个阶段**的计划变得清晰:首先是值类(当时还叫别的名字,稍后会详述),然后才是*专门的泛型*。我们将在第 6 节回到泛型,因为那是另一篇更长的论述。 如果你曾经尝试阅读关于 Valhalla 的文章却淹没在一堆矛盾的术语中,那不是你的错。这里命名改变了好几次,而且不是装饰性的:每次改名背后都代表着模型的改变。让我们追踪一下,因为这是展示*如何*设计这个功能的最佳方式。 [](https://substackcdn.com/image/fetch/$s_!SLqA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6176d42d-70e2-4032-93ce-569168be54a0_555x608.png) **阶段 1:值类型:** 最早的术语。含糊不清,因为还不清楚这些东西到底是什么。 [](https://substackcdn.com/image/fetch/$s_!lDtT!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcf1a00b3-2a2f-4fc1-995c-046e48ed294d_746x472.png) **阶段 2:内联类:** 大约在 2019–2020 年,一种区分一直延续到今天:类被分为**身份类**(具有身份,即我们至今所知的一切)和新的**内联类**(无身份)。那时提出了“写起来像类,用起来像 int”的口号,并设定了基本约束:内联类默认是 final 的,它们的字段是 final 的,不能在上面同步。 **阶段 3:“原始类”与双投影模型。** 这里变得有趣了,因为这正是那个*被大幅削减*的想法。在 2021 年的“State of Valhalla”文档中,Valhalla 承诺了三样东西:*值对象*、*原始类*和*专门的泛型*。“原始类”的想法是一个单一类型会有**两个投影**:一个值变体(扁平、不可为 null,像基本类型一样)和一个引用变体(允许 null 的装箱)。在各个迭代中,这被写作 `Point.val`/`Point.ref`,后来他们实验过 `Point!` 和 `Point?` 语法。这个模型很强大,但**心理负担很重**。程序员每天要应对同一类型的两种形式,并理解它们之间的转换何时发生。团队秉承着“为用户简化模型,即使以性能上限为代价”的教训,最终**拆除了**这种二元性。 **阶段 4(今天):“值类”和“值对象”。** 当前的 [**JEP 401**](https://openjdk.org/jeps/401),由 Dan Smith 撰写(审阅者:Brian Goetz),将其简单化。有一个新东西:一个**值类**,用 `value` 修饰符声明。它的实例是**值对象**:没有身份的对象。并且(关键点)*值类仍然是一个引用类型*。整个棘手的非空性问题已被**分拆到单独的、可选的 JEP**([Null-Restricted Value Class Types](https://openjdk.org/jeps/8316779)),我们稍后会提到。所以我们不是有一个复杂的概念,而是有两个简单、正交的概念:“它有身份吗?”以及,稍后,“它允许 null 吗?”值得记住,因为如果你遇到一篇旧文章(或描述“原始类”为独立机制的 **Baeldung**),你读的是过时的模型。在 OpenJDK 的正典中,那个意义上的“原始类”已不复存在。 沿途还有更多东西被舍弃。原始的“值对象”JEP 草案被撤回,被 JEP 401 取代。原始的 [“通用泛型”草案](https://openjdk.org/jeps/8261529) 也回去重写了。JEP 401 伴随着 [**JEP 402:增强的基本类型装箱**](https://openjdk.org/jeps/402)(也是预览),加上整个系列的早期构建(LW1, LW2, LW3...)以及 JVM 语言峰会的演讲,包括 [**Frédéric Parain** 关于堆展平](https://inside.java/2025/10/31/jvmls-jep-401/) 和 [**Daniel Smith** 关于新的对象初始化模型](https://inside.java/2025/07/27/javaone-object-initialization/)。 这一节的主旨是:十二年的时间不是“写代码”的十二年。而是**摒弃想法**的十二年,直到留下一个真正可维护的方案。 让我们进入具体细节。以下是我们得到的确切内容。 **声明。** 通过添加 `value` 修饰符来创建值类: ```java value class USDCurrency implements Comparable<USDCurrency> { private int cents; // 隐式 final public USDCurrency(int dollars, int cents) { this.cents = dollars * 100 + cents; } public USDCurrency plus(USDCurrency that) { return new USDCurrency(0, this.cents + that.cents); } // dollars(), ...

相似文章

Value Classes 仍需编译器的关怀

Hacker News Top

本文讨论了 Value Classes 作为 JDK 28 中的预览功能的集成,并分析了 JVM 中的优化机会和限制,着重强调了谨慎的编译器支持的必要性。

JDK 27 已发布

Lobsters Hottest

JDK 27 作为 Java SE 平台版本 27 的参考实现已发布,包含的更新包括将 G1 设为默认垃圾回收器和 TLS 1.3 的后量子混合密钥交换。

Java 27 发布

Hacker News Top

Java 27 (JDK 27) 现已正式发布,主要增强功能包括将G1设为默认垃圾回收器、为TLS 1.3提供后量子混合密钥交换以及多种预览功能。