Project Valhalla 解读:十年耕耘终入 JDK 28
摘要
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(), ...
相似文章
@0xlelouch_: 2026年Java的90%精髓在于掌握以下10个概念。其他都是语法琐事。
一条推文列出了2026年Java的10个核心概念,涵盖JVM内存模型、并发、集合、I/O、异常、性能、工具链、调试、安全性和可观测性。
Odin dev-2026-06 发布
Odin dev-2026-06 已发布。Odin 是一种面向数据的编程语言,专为高性能系统开发而设计。
Blaise v0.10.0:本地后端、线程与增量编译
Blaise v0.10.0 通过 QBE 增加了本地后端支持、线程功能和增量编译,推动这款现代 Object Pascal 编译器向自托管和更广泛的平台支持迈进。
jj v0.41.0 发布
Jujutsu (jj) v0.41.0 已发布,这款实验性版本控制系统迎来了更新,旨在提升易用性和冲突处理能力。
Erlang/OTP 29.0 发布
Erlang/OTP 29.0 是一个重要版本,引入了原生记录、多值推导式、改进的编译器警告,以及增强的安全默认设置(如禁用 SSH 守护进程)。此外,该版本还包含了 JIT 改进以及诸如后量子加密支持等实验性功能。