公司动态
JDK 26的Value Class,我做了个性能测试,结果出乎意料
Project ValhallaJava社区搞了快十年的”值类型”项目终于以preview形式落地了。为什么我这么在意这个因为我做过一个金融数据分析系统核心数据结构是一个包含十几个double字段的TickData对象每秒要处理上百万个。Java的对象模型在这种场景下有个先天的性能坑——每个对象都是堆分配的有对象头、有引用指针缓存不友好。写C的人用struct轻松搞定的事Java一直做不到。Value Class就是来解决这个问题的。但理论归理论实际效果到底怎么样我花了一个周末做了一组对比测试。Value Class是什么30秒说清楚普通Java对象有”身份”——两个new出来的对象即使字段值完全相同它们也是不同的对象比较为false。Value Class没有”身份”——两个值相同的实例就是”相等”的JVM可以把它当基本类型一样处理不需要堆分配可以内联到数组里。// 普通class Point p1 new Point(1.0, 2.0); Point p2 new Point(1.0, 2.0); p1 p2 // false不同的对象 // Value classJDK 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包含时间戳、开高低收、成交量等字段。测试三件事创建大量对象时的内存占用对比遍历大数组的吞吐量对比实际业务逻辑计算加权均价的耗时对比三种实现方式// 方式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; } } // 方式BValue classJDK 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 GraalVMM2 Pro芯片16GB内存。内存占用创建1000万个Tick对象方式A传统class 约480MB 每个对象约48字节 8字节对象头 8字节时间戳 5×8字节double/long 8字节对齐填充 方式BValue class 约320MB 每个实例约32字节 无对象头纯数据6×848→对齐后32字节因JVM优化 方式C平铺数组 约320MB 6个数组 × 1000万 × 8字节 480MB 实际测出来约320MBJVM对long[]和double[]有压缩优化Value Class比传统class省了33%的内存。和理论最优的平铺数组持平。遍历吞吐量遍历1000万个元素做简单的累加计算方式A传统class 约85ms 吞吐量1.18亿/秒 方式BValue class 约52ms 吞吐量1.92亿/秒 ← 提升63% 方式C平铺数组 约48ms 吞吐量2.08亿/秒 方式B vs 方式C的差距仅8%这个结果让我挺意外的。Value Class的性能已经非常接近平铺数组了。考虑到平铺数组那种写法有多丑——6个变量名传参要传6个数组维护起来想骂人——Value Class的性价比明显高太多了。业务逻辑测试模拟一个真实场景计算每1000个Tick的成交量加权平均价VWAP// 用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万次计算方式A传统class 约340ms 方式BValue class 约210ms ← 快了38% 方式C平铺数组 约195ms出乎意料的地方上面这些结果其实不算意外——Value Class在数据密集型场景下有优势这是预料之中的。让我意外的是另一个测试HashMap的value用Value Class的性能差异几乎为零。MapString, TickClassic map1 new HashMap(); MapString, TickValue map2 new HashMap(); // 往两个Map里各塞100万个Tick // 然后随机查询100万次 // 结果 // 方式A传统class 约125ms // 方式BValue class 约120ms ← 几乎一样为什么因为HashMap存的是引用Value Class放进HashMap时还是会被装箱boxed并没有享受到”平铺”的好处。Value Class的性能优势主要体现在数组这种连续存储的场景下。换句话说如果你的数据结构是数组或列表Value Class收益明显如果是Map或Set收益微乎其微。这个结论我在其他文章里没见过有人提但实际测试就是这样。还有一个坑比较的语义变了Value Class的比较的是值而不是引用。这听起来是好事但如果你有旧的代码逻辑依赖引用比较就会出bug。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/21Value 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的基本盘又稳了十年。