返回

文章详情

项目瓦尔哈拉的解释:十年的工作如何在JDK 28中到来

Hacker News2026年6月19日 06:35

2023年6月15日,甲骨文工程师洛伊斯·福尔坦确认了行业中许多人已停止相信的事情:JEP 401:值类和对象将被集成到主OpenJDK存储库中,目标是JDK 28。这个变更之大,以至于其余的提交者被要求在集成期间暂停更大的提交。仅这个拉取请求就增加了超过197,000行代码,涵盖1,816个文件。然而,在我们庆祝之前:这只是预览功能,默认情况下禁用,正如布莱恩·戈茨迅速让大家冷静下来所说的,“只是瓦尔哈拉的第一部分。”戈茨还提出了一个很好的观察,即“他们永远不会发布它”的人群现在会顺利转向“但他们没有发布最重要的部分”(社区中流传已久的一个笑话是,我们更有可能自己进入瓦尔哈拉,而不是这个项目发布)。你必须赢得自己的仇恨者。因此,这是讲述整个故事的好时机。这个问题是一次深入的探讨,假设你之前从未关注过瓦尔哈拉的工作:从2014年的问题开始,通过理念的演变(相当数量的想法最终被丢弃),直到我们将在JDK 28中获得的确切内容。给自己泡杯咖啡。我已经把这个版本准备了很长时间,为的就是这个时刻。瓦尔哈拉从一开始就传达的口号是:“像类一样编码,像 int 一样工作。”一句话概括了项目的全部要点:我们希望编写正常的、可读的有方法、构造函数验证和合理字段名称的类,但我们希望JVM能够像处理原始类型一样高效地处理它们。要理解这是什么问题,你必须追溯到Java的基础。在这门语言中,除了八种原始类型(int、long、double、boolean及其他),所有东西都是引用类型。当你写 `Point p = new Point(1, 2)` 时,变量p并不是一个点。变量p是一个指针,一个衣物检号:在堆的某个地方有一个对象,而你手中握着一个纸条,上面是它的地址。每当你想读取一个字段时,JVM都必须“去衣物检票处”,通过指针(指针间接)进行跳跃。对于单个对象来说,这没什么问题。问题出现在规模上。堆上的每个对象都有自己的头部(十几个字节的元数据:其中包括JVM知道它是什么类型以及是否有人在对其进行同步等信息)。顺便提一下,这正是项目Lilliput最近试图解决的问题,帮助缩小对象头的大小。但头部大小并不是一切。每个对象都必须被分配,并在后面进行垃圾回收。而且,由于对象散布在堆上,实质上,一百万个Point的数组是将一百万张纸条指向散落在整个仓库的一百万个箱子的集合。布莱恩·戈茨在他的“瓦尔哈拉状态”文档中称这种内存布局为“蓬松” : 充气,臃肿。我们梦想的是一种紧凑的布局,即数据并排放置。为什么密度很重要?因为硬件的变化速度快于Java。1995年,内存访问的成本大致相同于CPU操作。今天,CPU的速度比主内存快两个数量级,而整个差距则由缓存来弥补。处理器以称为缓存行(通常64字节)的块读取内存。如果数据密集且有序,这样一块可以同时带来大量有用的值。如果我们每次都要跳跃到指针上,每次访问都冒着缓存未命中的风险,这可能比命中慢一百倍。这是引用的局部性,也是这整个游戏中的真正利害关系。“但是JVM有逃逸分析,”某个敏锐的人会说。没错:虚拟机可以识别出某些对象从不“逃逸”超出局部代码碎片,然后它根本不会分配它。从程序员的角度来看,似乎对象存在,但实际上它的字段被分散到普通变量或CPU寄存器中。在最佳情况下,分配的成本和后续垃圾回收的成本几乎降至零。问题是这种优化是不可预测和脆弱的。它只有在JIT编译器可以高可信度地跟踪对象的整个流时才有效。但只要对象落入另一个类的字段中,被存储在数组中,被传递到更复杂的方法中,或出现在JIT能够分析的代码边界之外,整个技巧就会失效。源代码保持不变,但性能行为可能会发生剧烈变化。这正是为什么经验丰富的JVM程序员将逃逸分析视为良好的附加功能,而不是项目基础的原因。如果应用程序的性能取决于特定JIT版本是否成功应用此优化,极容易陷入难以预测的回退陷阱。一次小的重构,一个JDK的更新……

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡