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

相关推荐
冷雨夜中漫步9 分钟前
DeepSeek Harness:一切皆插件的 AI Agent 框架深度解析
java·人工智能·ai·开源·github
IT_陈寒9 分钟前
Vite热更新失效?八成是这个配置在搞鬼
前端·人工智能·后端
qq_225891746618 分钟前
基于Flask的空气质量监测与预测分析系统
开发语言·后端·python·信息可视化·django·flask
LayZhangStrive19 分钟前
提示词沉淀 - 使用豆包时平时提问题
面试·职场和发展·prompt·提示词·豆包
lhldsg32 分钟前
社区健身系统开发实战:从需求分析到落地部署全流程指南
java·开发语言·小程序·需求分析
覆东流40 分钟前
4.Java运算符与表达式
java·开发语言·后端
JacksonMx1 小时前
ContentCachingRequestWrapper 实战:解决请求体“只能读一次”的官方方案
java·spring boot·spring
马丁玩编程1 小时前
GitHub 3.5k Star 之后,Ragent AI 框架新版本来了
后端·面试·github
微石科技1 小时前
医疗设备维保如何从被动维修转向预防管理?宁波微石科技让运行数据提前发信号
java·后端·struts
AINative软件工程1 小时前
LLM 应用的 Warm-Up 工程实践:冷启动延迟从 12 秒砍到 800ms 的 5 个工程手段
后端·llm·ai编程