紧凑对象头:从 96/128 位到 64 位
学完你要能自己回答
- 对象头承担哪些职责?为什么旧布局是 96 或 128 位?
- 64 位紧凑头如何同时放下类指针、identity hash、锁状态和 GC 年龄?
- JEP 519 转正后,JDK 25 默认开启了吗?启用条件是什么?
- 紧凑对象头对堆占用、GC、启动和工具链分别有什么影响?
- 怎样评估一个服务是否应该开启紧凑对象头?
对象头是四个子系统的交界01
堆里的每个对象都有一段固定头部。HotSpot 用这段头部同时服务四件事:GC 的标记/转发与年龄、类型系统的方法调用与类型检查、锁状态、以及一旦计算就稳定的 identity hash。头部大小与对象类型、数组长度、字段内容无关。
- GC:转发指针、对象年龄、标记状态。
- 类型系统:指向 Klass 的类指针,方法调用、反射、类型检查都要用。
- 锁:轻量锁/重量锁的状态与监视器信息。
- identity hash:
System.identityHashCode第一次计算后要保持稳定。
| 64 位 HotSpot 布局 | 组成 | 头部大小 |
|---|---|---|
| 压缩类指针(常见) | mark word 64 位 + compressed class 32 位 | 96 位 / 12 字节 |
| 未压缩类指针 | mark word 64 位 + class 64 位 | 128 位 / 16 字节 |
| 紧凑对象头 | 单一 64 位头,类指针压缩进头内 | 64 位 / 8 字节 |
| 32 位平台 | 不在 JEP 450 目标内 | 本来就更小;25 已移除 32 位 x86 |
旧布局把头部拆成 mark word 和 class word。mark word 会被锁、hash、GC 复用/覆盖;class word 相对稳定,保证类型信息始终可读。紧凑布局则把两者合并,把压缩类指针放进同一个 64 位字里。
省下的 4–8 字节,为什么值得做02
Java 对象普遍偏小。Project Lilliput 的实验显示,很多工作负载的平均对象大小只有 32–64 字节,头部可能占活数据的 20% 以上。每个对象省 4–8 字节,乘以对象数量,就是可观的堆占用与缓存局部性收益。
| 维度 | 预期变化 | 说明 |
|---|---|---|
| 堆占用 | 下降 | 对象越小人越多,比例收益越大 |
| GC 压力 | 通常下降 | 活数据变少,标记/复制工作量降低 |
| CPU / 吞吐 | 基准中持平或改善 | JEP 519 引用 SPECjbb2015 等结果 |
| 延迟 | 不应显著变差 | JEP 450 目标是不引入超过 5% 的偶发开销 |
| 工具兼容 | 需要验证 | 依赖头部布局的 agent / 分析工具要回归 |
| 默认行为 | 不变 | JEP 519 不把紧凑头设为默认 |
JEP 519 引用的数据包括:SPECjbb2015 堆空间少 22%、CPU 时间少 8%;GC 次数少 15%;高并行 JSON parser 快 10%。这些是特定基准的结果,不是你的服务承诺。JEP 450 早期采用者报告的活数据下降约 10%–20%,同样取决于对象分布。
64 位头怎么塞下类指针、hash、锁与 GC01
紧凑布局取消了 mark word 与 class word 的分工,把压缩类指针直接并入 64 位头。类指针从 32 位进一步压缩到 22 位;identity hash 保持 31 位;另外预留 4 位给 Project Valhalla,剩余位用于 GC 年龄、锁标记与转发标记。
- 类指针:22 位压缩类指针,要求
UseCompressedClassPointers开启。 - identity hash:31 位,大小与旧布局相同;一旦计算仍保持稳定。
- 锁:轻量锁/监视器锁通过 tag 位表示,不再用带标签指针覆盖整个 mark word,因此类指针始终保留。
- GC:转发时需要保留类指针,引入 self-forwarded tag 等机制;具体编码属于实现细节。
- Valhalla:预留 4 位,为未来值类型留空间。
这意味着锁、hash、GC 不能再像旧布局那样随意覆盖整个 mark word。实现必须在 64 位内协调这些状态,代价是更复杂的转发与锁路径;收益是每个对象固定少 4–8 字节,并且类指针与对象数据更接近,缓存局部性更好。
启用路径:产品特性,但默认关闭02
JEP 450 在 JDK 24 以实验特性引入,需要 -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders。JEP 519 在 JDK 25 把它转为产品特性:不再需要解锁实验选项,但默认值仍是 false。
java -XX:+PrintFlagsFinal -version | grep UseCompactObjectHeaders
bool UseCompactObjectHeaders = false {product lp64_product} {default}
java -XX:+UseCompactObjectHeaders -XX:+PrintFlagsFinal -version | grep UseCompactObjectHeaders
bool UseCompactObjectHeaders = true {product lp64_product} {command line}
它依赖压缩类指针。若同时关闭 UseCompressedClassPointers,JVM 会警告并自动禁用紧凑对象头——不是启动失败,而是静默回退,所以必须核对最终 flag 值。
java -XX:+UseCompactObjectHeaders -XX:-UseCompressedClassPointers \
-XX:+PrintFlagsFinal -version 2>&1 | grep -E "warning|UseCompactObjectHeaders"
OpenJDK 64-Bit Server VM warning: Option UseCompressedClassPointers was deprecated in version 25.0 and will likely be removed in a future release.
OpenJDK 64-Bit Server VM warning: Compact object headers require compressed class pointers. Disabling compact object headers.
bool UseCompactObjectHeaders = false {product lp64_product} {command line}
评估步骤:先基线,再 A/B01
紧凑对象头的收益高度依赖对象图。正确做法不是「看到转正就开」,而是用同一份构建、同样的堆上限和负载,对比默认布局与紧凑布局。
- 记录基线:堆占用、GC 次数/停顿、分配速率、P99 延迟、CPU。
- 只加
-XX:+UseCompactObjectHeaders,保持堆上限、GC、线程数不变。 - 用 PrintFlagsFinal 确认最终值为 true,排除被其他参数静默禁用。
- 跑真实流量或可复现的压测,至少覆盖启动、稳态、峰值和一次 Full GC。
- 回归 native agent、JFR/堆转储分析、锁与对象布局相关工具。
- 灰度发布,保留关闭开关;如果延迟或 CPU 变差,能快速回退。
java -XX:+PrintFlagsFinal -version | grep UseCompactObjectHeaders
java -XX:+UseCompactObjectHeaders -Xlog:gc*:file=gc.log -jar app.jar
评估时不要只看「堆小了」。堆占用下降可能伴随对象搬移、TLAB 大小、GC 区域利用率的变化;延迟和 CPU 才是是否上线的决定因素。
常见坑02
- 把转正当默认:25 默认仍是 96/128 位,必须显式开启。
- 关闭压缩类指针:紧凑头会被静默禁用,只留下 warning。
- 在 JDK 24 忘记解锁实验选项:24 需要 UnlockExperimentalVMOptions,25 不需要;脚本要按版本区分。
- 用 Unsafe 假设头部偏移:布局是运行时选择,不是 API 契约。
- 只看堆占用:还要看 GC 停顿、CPU、P99 和启动时间。
- 忽略工具链:native agent、堆分析器、锁监控工具都要回归。
- 以为能解决所有内存问题:字段、数组内容、对象图引用才是大头;头部只是固定开销。
合上页面,先回答01
先猜再看紧凑对象头是 4 字节吗?
先猜再看JDK 25 默认开启紧凑对象头吗?
01. 对象头承担哪四类职责?
02. 为什么紧凑头依赖压缩类指针?
03. 紧凑头会改变 class 文件或 Java API 吗?
04. 什么样的负载最可能受益?
版本与边界一页表02
| 事项 | JDK 24 | JDK 25 |
|---|---|---|
| 支持级别 | 实验特性 | 产品特性(JEP 519) |
| 开启方式 | UnlockExperimentalVMOptions + UseCompactObjectHeaders | 只需 UseCompactObjectHeaders |
| 默认值 | false | false(JEP 519 非目标:不改默认) |
| 头部大小 | 64 位(开启时) | 64 位(开启时) |
| 压缩类指针 | 必需 | 必需;关闭则紧凑头被禁用 |
| 32 位 x86 | 仍支持 | 已移除(JEP 503) |
一句话总结:紧凑对象头是 25 的「可用产品特性」,不是「默认行为」。它把固定开销从 12/16 字节压到 8 字节,收益真实但依赖对象分布;上线前用 flag 核对、压测和灰度,而不是相信基准数字。