Echo's blog
Echo's blog
· 1 min read · 资讯

Project Valhalla十年的等待

Project Valhalla 十年的等待#

你每天付出的”装箱税”#

装箱税

写 Java 的人,多半有过这样的瞬间:盯着一个 List<Integer> 发呆,心里隐隐发毛。一个 int 只有 4 字节,包进 Integer 之后,对象头 12–16 字节,对齐填充再来几字节,一个数字活活吃掉 20+ 字节。这还不是最糟的。数组里存着几十万个 Integer,每个都指向堆上各自的对象——GC 扫描它们,CPU 在它们之间跳来跳去,缓存行里塞满了毫无用处的指针而非数据本身。

这叫”装箱税”(boxing tax)。Java 从 1.5 引入自动装箱以来,每个 Java 开发者都在交这笔税,只不过大多数时候感觉不到。

Optional 是另一个重灾区。一个 OptionalInt,你只是想表达”可能有也可能没有”,结果 JVM 给你分配了一个堆对象。如果某条热路径上有百万次调用,GC 压力瞬间飙升。为什么一个简单的”存在或不存在”非要走堆分配?因为没有别的路可走。

用 C 或 C++ 的程序员读到此处大概会摇头——值不就是值吗,怎么在 Java 里就变成了对象的附属品?

这个问题的答案,是 Java 类型系统二十多年来最根本的设计决策:一切皆对象 ( 除了一等公民 int、long、float、double 等八个基本类型 )。你无法定义自己的”值类型”,无法让一个数据容器像 int 一样活在栈上或内联在数组里。如果你想要一个”轻量级的整数包装”,对不起,只能 new 一个对象出来。

Project Valhalla 用十年时间,把这个决策推翻了。

去身份化:Valhalla 改变了什么#

去身份化

Valhalla 的核心概念是”无身份的值类型”(identity-free value types),在 Java 术语里叫”原始类”(primitive classes)。一个原始类的实例没有对象身份——它没有”我是谁”这个概念,只是一堆比特。你没法对它做 == 比较 ( 除了按位比较 ),没法用它做 synchronized 锁,没法把它传给需要 identity 的 API(System.identityHashCode 之类 )。作为交换,它获得了和 int 一样的内存布局。

用 JEP 401 中展示的例子来说:

PRTCL // JAVA
primitive class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}

这段代码定义了一个 Point,它的大小就是两个 int——8 字节,外加可能的对齐填充,远小于一个普通对象的 16+ 字节头。更关键的是,Point[] 数组里存的是实实在在的坐标数据,而不是指向堆对象的引用。遍历一个 Point[],CPU 从缓存行里顺序读入数据,不存在指针间接访存的延迟。

你可以用 analog 来理解:普通 Java 对象像一本书的目录页,上面写着”第 42 页”,你得翻到第 42 页才能看到内容。原始类实例则是直接把内容写在目录页上——不必翻页。

这个变更波及了 JVM 的每一层:

  • JVM 规范:类文件格式新增 ACC_PRIMITIVE 标志,引入新的字段描述符。
  • GC:垃圾收集器必须知道哪些类型没有 identity,不再需要为它们维护对象头、标记位和锁信息。
  • JIT 编译器 (C1/C2):C2 的重写尤其复杂——它需要为原始类生成不同于普通对象的标量替换和寄存器分配策略。
  • Java 语言规范:新的 primitive class 声明语法,以及相应的类型转换规则。
  • 标准库Optional 系列、Stream 管道中的中间容器、集合框架中的包装类——无数 API 需要或可以迁移到底层用原始类实现。

这是自 Java 5 引入泛型以来,JVM 类型系统最大的一次手术。

这对你的代码意味着什么#

代码变革

JDK 28 是第一个完整包含 Valhalla 特性的发行版。你不需要等一个”再一个十年”。升级 JDK,写一个 primitive class,剩下的交给 JVM。

但是实际迁移不会一蹴而就。几个值得关注的方向:

1. Optional 的终结

目前 OptionalIntOptionalLongOptionalDouble 已经比泛型 Optional<T> 节省了一些装箱开销,但它们仍然是堆对象。在 Valhalla 版本中,这些类可以声明为原始类——一个 OptionalInt 实例就是一个 int 加上一个布尔标志位,两个原生值排在一起,没有对象头。判断”是否有值”变成一次位检查,不需要解引用。

2. 数值计算与集合

如果你写过高性能计算相关的 Java 代码,一定遇到过这样的情况:不想用 double[] 因为太原始,用 List<Complex> 又承受不了装箱开销。原始类给了中间道路——你可以定义一个 Complex 原始类,然后用 Complex[] 存储。CERN 的 Java 科学计算库、金融领域的定价引擎、游戏引擎——这类对吞吐量和内存布局敏感的代码将从 Valhalla 获得最大收益。

3. 数据库与协议层

ORM 框架中大量存在”一个查询返回十万个行对象”的场景。每个行封装成一个完整的 Java 对象,GC 要扫描它们、回收它们。如果数据传输对象是原始类,这批对象的分配就是栈分配或内联数组分配,GC 几乎感觉不到存在。MyBatis、Hibernate 等框架未来很可能提供基于原始类的行映射模式。

4. 内存安全 vs. 性能

Valhalla 不会让 Java 变成 C。原始类有默认值 ( 所有字段按零初始化 ),不允许循环引用 ( 因为没有 identity,无法形成自引用图 ),字段类型有限制。你得到的是一种受管控的内联数据——比对象快,但不能像 sun.misc.Unsafe 那样随意解读内存。

十年磨一剑#

十年一剑

Project Valhalla 2009 年开始酝酿,2014 年左右正式立项。中间经历过 Value Types、Inline Types、Primitive Classes 多次命名变更和设计重构。OpenJDK 社区有专门的邮件列表讨论 Valhalla——那些邮件串的长度堪比小论文。贡献者们花了好几年时间说服自己:把值类型塞进现有的 JVM 架构,而不是重写一个 JVM。

JDK 28 的发布标志着一个节点。你仍然可以写 class Point,没人会阻止你。但当你写 primitive class Point 时,背后是十年间无数 JVM 工程师对 GC、C2 JIT 编译器、类加载器、调试器接口的逐一改动。这些改动藏在字节码背后,对应用层几乎是透明的——这正是工程艺术所在。

原文标题: Project Valhalla, Explained: How a Decade of Work Arrives in JDK 28 来源:Hacker News

Related Posts

Comments

Copied
Copied to clipboard