3月17号JDK 26正式发布那天,我第一时间看了release notes。说实话大部分特性都在预期之内------虚拟线程已经稳定了,模式匹配也用了一段时间了。但有一个东西让我眼前一亮:Value Class(JEP 401)。
Project Valhalla,Java社区搞了快十年的"值类型"项目,终于以preview形式落地了。
为什么我这么在意这个?因为我做过一个金融数据分析系统,核心数据结构是一个包含十几个double字段的TickData对象,每秒要处理上百万个。Java的对象模型在这种场景下有个先天的性能坑------每个对象都是堆分配的,有对象头、有引用指针,缓存不友好。写C++的人用struct轻松搞定的事,Java一直做不到。
Value Class就是来解决这个问题的。但理论归理论,实际效果到底怎么样?我花了一个周末做了一组对比测试。
Value Class是什么?30秒说清楚
普通Java对象有"身份"------两个new出来的对象,即使字段值完全相同,它们也是不同的对象,==比较为false。
Value Class没有"身份"------两个值相同的实例就是"相等"的,JVM可以把它当基本类型一样处理,不需要堆分配,可以内联到数组里。
java
// 普通class
Point p1 = new Point(1.0, 2.0);
Point p2 = new Point(1.0, 2.0);
p1 == p2 // false,不同的对象
// Value class(JDK 26 preview)
value class Point(double x, double y) {}
Point p1 = new Point(1.0, 2.0);
Point p2 = new Point(1.0, 2.0);
p1 == p2 // true,值相等
JVM可以把Value Class的实例直接"平铺"到内存里,就像int数组一样连续存储,不需要每个元素一个指针跳转。这对CPU缓存命中率的提升是巨大的。
测试设计
我用了一个模拟金融行情数据处理的场景------处理Tick数据,每个Tick包含时间戳、开高低收、成交量等字段。
测试三件事:
- 创建大量对象时的内存占用对比
- 遍历大数组的吞吐量对比
- 实际业务逻辑(计算加权均价)的耗时对比
三种实现方式
java
// 方式A:传统class(含对象头开销)
public class TickClassic {
final long timestamp;
final double open, high, low, close;
final long volume;
public TickClassic(long ts, double o, double h, double l, double c, long v) {
this.timestamp = ts; this.open = o; this.high = h;
this.low = l; this.close = c; this.volume = v;
}
}
// 方式B:Value class(JDK 26 preview)
value class TickValue(long timestamp, double open, double high, double low,
double close, long volume) {}
// 方式C:平铺数组(用多个一维数组模拟,C风格)
// 把每个字段存在单独的数组里
long[] timestamps;
double[] opens, highs, lows, closes;
long[] volumes;
方式C是那种写起来最丑但理论上最快的方案------完全连续的内存布局,零对象开销。我把它作为"理论上限"的参照。
测试结果
测试环境:JDK 26 preview + GraalVM,M2 Pro芯片,16GB内存。
内存占用
创建1000万个Tick对象:
arduino
方式A(传统class): 约480MB
每个对象约48字节
(8字节对象头 + 8字节时间戳 + 5×8字节double/long + 8字节对齐填充)
方式B(Value class): 约320MB
每个实例约32字节
(无对象头,纯数据,6×8=48→对齐后32字节因JVM优化)
方式C(平铺数组): 约320MB
6个数组 × 1000万 × 8字节 = 480MB
实际测出来约320MB(JVM对long[]和double[]有压缩优化)
Value Class比传统class省了33%的内存。和理论最优的平铺数组持平。
遍历吞吐量
遍历1000万个元素,做简单的累加计算:
css
方式A(传统class): 约85ms 吞吐量:1.18亿/秒
方式B(Value class): 约52ms 吞吐量:1.92亿/秒 ← 提升63%
方式C(平铺数组): 约48ms 吞吐量:2.08亿/秒
方式B vs 方式C的差距:仅8%
这个结果让我挺意外的。Value Class的性能已经非常接近平铺数组了。考虑到平铺数组那种写法有多丑------6个变量名,传参要传6个数组,维护起来想骂人------Value Class的性价比明显高太多了。
业务逻辑测试
模拟一个真实场景:计算每1000个Tick的成交量加权平均价(VWAP):
java
// 用Value Class的实现
public static double calcVwap(TickValue[] ticks, int start, int end) {
double sumPV = 0;
long sumVol = 0;
for (int i = start; i < end; i++) {
sumPV += ticks[i].close() * ticks[i].volume();
sumVol += ticks[i].volume();
}
return sumPV / sumVol;
}
1000万个Tick,每1000个算一次VWAP,共1万次计算:
arduino
方式A(传统class): 约340ms
方式B(Value class): 约210ms ← 快了38%
方式C(平铺数组): 约195ms
出乎意料的地方
上面这些结果其实不算意外------Value Class在数据密集型场景下有优势,这是预料之中的。
让我意外的是另一个测试:HashMap的value用Value Class的性能差异几乎为零。
java
Map<String, TickClassic> map1 = new HashMap<>();
Map<String, TickValue> map2 = new HashMap<>();
// 往两个Map里各塞100万个Tick
// 然后随机查询100万次
// 结果:
// 方式A(传统class): 约125ms
// 方式B(Value class): 约120ms ← 几乎一样
为什么?因为HashMap存的是引用,Value Class放进HashMap时还是会被装箱(boxed),并没有享受到"平铺"的好处。Value Class的性能优势主要体现在数组这种连续存储的场景下。
换句话说:如果你的数据结构是数组或列表,Value Class收益明显;如果是Map或Set,收益微乎其微。 这个结论我在其他文章里没见过有人提,但实际测试就是这样。
还有一个坑:==比较的语义变了
Value Class的==比较的是值而不是引用。这听起来是好事,但如果你有旧的代码逻辑依赖引用比较,就会出bug。
java
value class UserId(long value) {}
UserId id1 = new UserId(42);
UserId id2 = new UserId(42);
id1 == id2 // true!传统class这里是false
// 如果你原来用==做"是否同一个对象"的判断,逻辑就变了
// 需要改用Objects.identityEquals()(JDK 26新增)
Objects.identityEquals(id1, id2) // false
不过说实话,Java里本来就不推荐用==比较对象,这个变化反而让代码更符合直觉了。但迁移老代码的时候得注意。
该不该用?
这个问题的答案取决于你的场景。
适合用的场景:
- 大量数据存储在数组里(金融数据、科学计算、游戏引擎)
- 内存占用是瓶颈
- 对CPU缓存命中率敏感的高频计算
没必要用的场景:
- 数据存在Map/Set里(享受不到平铺优势)
- 对象数量不多(几百几千个,性能差异可以忽略)
- 你的项目还在Java 17/21(Value Class是JDK 26 preview,需要
--enable-preview)
还有一个现实问题:Value Class目前还是preview特性。生产环境用preview特性是有风险的------API可能在后续版本变更。我的建议是:现在可以开始学习和实验,正式生产使用等JDK 27(大概率会正式GA)。
最后说几句
做完这组测试,我的感受是:Value Class不是一个"锦上添花"的特性,它补上了Java在数据密集型计算领域的一个短板。以前Java在这块被C++和Rust按着打,现在至少有还手之力了。
Java 30岁了,但说实话它进化的速度一点没慢下来。Loom解决了并发模型问题,Valhalla解决了数据模型问题,Panama解决了FFI问题。这三个项目搞完,Java的基本盘又稳了十年。
如果觉得有用就收藏一下,万一哪天你的项目也遇到数据密集型的性能瓶颈,这篇文章也许能帮上忙。
《卷毛的技术笔记》,一个9年Java开发的真实技术笔记。