JDK 26的Value Class,我做了个性能测试,结果出乎意料

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包含时间戳、开高低收、成交量等字段。

测试三件事:

  1. 创建大量对象时的内存占用对比
  2. 遍历大数组的吞吐量对比
  3. 实际业务逻辑(计算加权均价)的耗时对比

三种实现方式

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开发的真实技术笔记。

相关推荐
Revolution611 小时前
数组本身没有 map,为什么还能直接调用:原型链怎样查找属性
前端·javascript·面试
swipe1 小时前
10|(前端转全栈)库存扣减为什么最容易出事故?SKU、并发与原子更新
前端·后端·全栈
smallYoung1 小时前
学习笔记-python基础(day12 正则表达式)
后端
Zane19941 小时前
volatile / synchronized / final:三大特性(原子性/可见性/有序性)到底谁保证了什么?
java·后端
巴勒个啦1 小时前
从需求到上线:记录一次完全由 AI 辅助完成的小产品全流程
java·前端
网易云信1 小时前
销售为什么是企业 AI 落地的"最佳突破口"?
人工智能·后端·agent
名字还没想好☜1 小时前
Spring @Async 不生效排查:自调用失效、默认线程池坑与异步方法里的异常去哪了
java·python·spring·异步
wechatbot8882 小时前
解决企业微信官方 API 短板:iPad 协议全功能对接方案
java·汇编·windows·http·微信·企业微信
Alan_752 小时前
文件下载中文文件名乱码终极方案
后端