Java 里很多对象其实不大。一个普通的 Integer、一条短字符串、一个简单的 DTO,真正装业务数据的字节往往只有几十,但每个对象还要再背一份「对象头」元数据。对象一多,头就变成堆里的固定税。
从 Java 27(2026-09-15 GA) 起,这件事有了默认答案:Compact Object Headers(紧凑对象头)正式默认开启。在 64 位 HotSpot 上,对象头从常见的 96 bit 缩到 64 bit。对小对象密集的服务来说,这往往不是「微调」,而是堆占用、GC 次数和缓存局部性一起动起来。
本文只谈 Java 语言与 JVM 实践:对象头是什么、默认变更带来什么、怎么量、升级时踩什么坑,并顺带提一句同版本里 G1 全面默认这件事。
对象头是什么,为什么要紧凑化
HotSpot 堆上的每个对象都有一块固定大小的 header,用来支撑:
- GC:年龄、转发指针等;
- 类型系统:类指针,用于虚方法、类型检查、反射等;
- 锁:轻量锁 / 重量锁状态;
- identity hash:
System.identityHashCode算出来后要落在头里。
在传统 64 位布局里,header 通常分成 mark word 和 class word。开启压缩类指针时,常见体积是 96 bit(12 字节);未压缩类指针时还会更大。Project Lilliput 相关实验里,不少真实负载的平均对象只有 32–64 字节量级,对象头单独就能占 live data 的两成左右。头大一点,等于同样业务数据要占更多堆,也更容易把相邻对象挤出缓存行。
Compact Object Headers 把压缩后的类指针「塞进」原来的 mark word,去掉独立的 class word,把 64 位平台上的对象头压到 64 bit(8 字节)。锁与 GC 转发协议也做了配套调整,保证类型信息仍可直接从 header 读到。这项能力来自 Project Lilliput:JDK 24 以实验特性登场(JEP 450),JDK 25 产品化(JEP 519),JDK 27 用 JEP 534 改成默认。
Java 27:默认开启,不用再手写那一行
JDK 25 / 26 时代,你需要显式打开:
java -XX:+UseCompactObjectHeaders ...到了 JDK 27,这行可以删了——默认就是开的。若要临时回退到旧的 96 bit 布局,用:
java -XX:-UseCompactObjectHeaders ...官方 release notes 写得很清楚:-XX:-UseCompactObjectHeaders 计划在未来版本弃用并移除。也就是说,回退只适合做对照实验或排查兼容性,不宜当成长期配置。
想确认当前进程里这个开关的实际值,可以用 HotSpot 常见的 flag 打印方式(以你所用发行版的文档为准):
java -XX:+PrintFlagsFinal -version 2>&1 | grep UseCompactObjectHeaders另有一点和 CDS 启动有关:JDK 镜像里为开启紧凑对象头准备了对应的 CDS 归档(如 classes_coh.jsa),默认路径与关闭紧凑头时用的归档不同。关掉 UseCompactObjectHeaders 时,JVM 会走另一套归档,以免启动性能被拖垮。升级脚本如果硬编码了旧的 CDS 路径,值得扫一眼。
收益:堆更密,局部性更好,GC 压力常会降
JEP 534 与 Oracle / OpenJDK 材料里给出的官方实验数字,方向一致:
| 场景 | 结果(官方实验) |
|---|---|
| SPECjbb2015(某一配置) | 堆约 少 22%,CPU 时间约 少 8% |
| SPECjbb2015(另一配置) | GC 次数约 少 15%(G1 / Parallel 都观察到) |
| 高并行 JSON 解析微基准 | 运行时间约 少 10% |
Inside.java 对运行时更新的归纳,也把整体堆占用下降粗算到大约 20% 这一档。真实业务会落在区间里:小对象多、对象图深、DTO / 集合节点密集的服务,更容易吃到接近上述数字的收益;已经很大的数组或「胖对象」为主的负载,相对收益会小一些——因为头在总对象体积里的占比本来就低。
为什么「头变小」常会连带变快?
- 堆密度上升:同样 live set 占更少字节,同样
-Xmx能多塞业务对象,或反过来用更小堆扛住同等负荷。 - 缓存局部性:对象更「挤」,扫描、遍历、序列化时触及的缓存行往往更少。
- GC 压力下降:同样分配速率下,堆更快触顶的概率下降,收集次数与停顿预算都有机会改善。上面 SPECjbb 的「少 15% GC」就是这条链路的旁证。
需要强调:这些是 吞吐 / 堆占用 类收益,不是语言语法变更。应用代码不用改;换的是 JVM 如何摆对象。
怎么量:别只看 Slogan,做一次 A/B
升级到 Java 27 后,建议把紧凑对象头当成一次可对照的默认变更来验证,而不是假定「一定省 20%」。
对照思路很简单:
- 同一构建、同一负载、同一堆上限;
- 一组用默认(紧凑头开);
- 一组显式
-XX:-UseCompactObjectHeaders; - 对比:常驻堆 / 老年代占用、Full GC 或混合收集频率、分配速率、p99 延迟、容器 RSS。
常用观测手段即可,不必新学一套工具:
- GC 日志(Unified Logging):
-Xlog:gc*,看 pause、heap after GC; - JFR:录一段稳定流量,对比
jdk.ObjectAllocation*、GC 相关事件; - 容器侧:RSS / working set,看「堆小了但本地内存涨了」这种错位有没有出现。
若你习惯用 Java Object Layout(JOL)这类库看单个实例 layout,升级后可以抽几个关键类型看一眼 header 是否从 12 字节变为 8 字节——但以 整机负载 A/B 为准;单对象微观测只能解释「为什么堆变了」,不能代替压测。
Amazon、SAP 等已在生产与下游发行版里大规模验证过紧凑对象头(含回迁到更早 LTS 的实践)。对大多数业务来说,默认开是稳妥方向;你要做的是确认 自己的 KPI 曲线符合预期。
升级与观测时要注意的点
1. 回退开关是过渡手段
-XX:-UseCompactObjectHeaders 能关掉,但官方已标明未来会弃用移除。若某第三方 native agent、老旧 SA/诊断脚本、或自研堆解析工具对旧 header layout 有硬编码假设,应推动工具适配,而不是永久关掉紧凑头。
2. 和压缩类指针、超大堆的边界
紧凑对象头依赖压缩类指针编码(类指针进一步压到更窄的位宽)。JDK 27 里 -XX:[+/-]UseCompressedClassPointers 已变为 obsolete:JVM 始终 使用压缩类指针,再传这个选项只会得到警告。这和「紧凑头成为默认」是同一条对象布局演进线上的事。
早期 JEP(450)还提到:在非 ZGC、堆超过约 8TB 等极端配置下,紧凑头可能无法启用或会被自动关掉。普通云上服务几乎碰不到;若你维护超大堆或非常规 GC 组合,请以当前发行版 release notes 与启动日志为准,不要假设「默认开了就一定在所有配置下都开」。
3. 诊断与 CDS
- 依赖对象偏移、手工解析 mark word 的诊断代码需要回归;
- 自定义 CDS / AOT 流程要确认与
*_coh*.jsa一类归档匹配; - 对比实验时,保证两组除紧凑头开关外,GC、堆大小、大页、压缩指针相关环境一致,否则分不清收益来源。
4. 预期管理
「默认省内存」不等于「可以无脑砍一半 -Xmx」。先量 live set 与 GC 曲线,再决定是否下调堆或提高副本密度。对延迟敏感服务,重点看 p99 是否因 GC 减少而改善,有没有个别路径因锁/转发协议变化出现回归(官方目标是开销很小且少见,但仍应用你的关键路径压一遍)。
并列提示:G1 在 Java 27「到处默认」
同一次 GA 里,还有 JEP 523:G1 成为所有环境下的默认 GC。过去在单 CPU、物理内存偏小(历史上常见阈值是约 1792MB)等受限环境,JVM 可能默默选 Serial;现在不指定 GC 时 一律 G1。Serial 仍在,可用 -XX:+UseSerialGC 显式选择。
这和紧凑对象头是两条线:一个改 对象怎么摆,一个改 默认用谁收垃圾。小堆容器、sidecar、CLI 工具若从未写过 GC 参数,升级后可能同时吃到「头更小 + 默认 G1」两套变化。排查性能时,建议:
- 先确认
UseCompactObjectHeaders与实际 GC(PrintFlagsFinal/ GC 日志); - 需要排除 GC 干扰时,显式固定
-XX:+UseG1GC或-XX:+UseSerialGC再做紧凑头 A/B。
不要把两者的收益或回归混成一笔账。
小结
Java 27 把 Compact Object Headers 从「可选项」推进成「默认布局」:64 位上对象头 96→64 bit,换来更高堆密度、更好局部性,以及官方基准里可见的堆占用与 GC 次数下降。对多数服务,这是一次几乎零代码的内存与运行时优化;你需要做的是 量自己的数字,并记住回退 flag 只是过渡、旧布局会走向弃用。
顺手记住同版本的 G1 全面默认,但别让它抢走对对象布局变更的注意力——先搞清楚对象为什么变小了,再决定堆与 GC 策略怎么跟着调。
参考
- JEP 534: Compact Object Headers by Default
- JEP 519: Compact Object Headers
- JEP 450: Compact Object Headers (Experimental)
- JEP 523: Make G1 the Default Garbage Collector in All Environments
- The Arrival of Java 27(Oracle Java 博客)
- JDK 27 Runtime Updates(Inside.java)
- JDK 27 Release Notes — Compact Object Headers Are Enabled by Default
- Phoronix: Java 27 Reaches GA…