JDK 27 升级后,紧凑对象头怎么做 A/B 对比
JDK 27 已于 2026-09-15 GA。随版本交付的 JEP 534(Compact Object Headers by Default,Closed/Delivered)把紧凑对象头(COH)做成 HotSpot 默认开启 。默认值怎么变、小容器上 GC/堆缩放要注意什么,已在前作 《JDK27升级后小容器GC和内存默认值变了》 写过;本篇不复述那份清单,只谈一件事:升完之后,怎么把「开 COH」和「换 GC」拆成单变量,做可复现的 A/B。
升级现场最容易犯的错,是一次改完:默认 G1 + 默认 COH + 新 CDS,然后对着 RSS、尾延迟、启动时间说「JDK 27 整体变快了 / 变慢了」。变量搅在一起,结论没法复用。Inside.java 2026-09-28 的 JDK 27 Performance Update 写得很直:先测新默认,再一次只改一个变量 ------例如只关 -XX:-UseCompactObjectHeaders,或只换收集器。
痛点:升级后「变了」说不清是谁的锅
常见三种糊成一团的对比:
- 基线是 JDK 26 + Serial(或未钉死 GC),对照是 JDK 27 默认(G1 + COH)。版本、GC、对象头一起变。
- 同一镜像里同时 关 COH、改
-XX:+UseSerialGC、还换了 CDS 档案,最后只报一个「关了紧凑头之后 RSS 降了」。 - 把 JEP 534 Motivation 里 SPECjbb2015 某配置约 22% 堆、8% CPU 当成自家 SLA,上线前不做同负载实测。
本篇范围很窄:两组进程钉死同一 GC、同一堆边界、同一 CDS 策略 ,唯一差别是 ±UseCompactObjectHeaders;要看旧 Serial 行为就另开一组,别和关 COH 绑在同一轴上。虚拟线程把瓶颈从「线程数」挪到「池与调度」的经验,见 《Java 虚拟线程瓶颈转移》;COH 的 A/B 同理------先隔离变量,再谈数字。
JEP 534:默认开,临时关,计划弃用而非已弃用
JDK 27 release notes(Compact Object Headers Are Enabled by Default,JDK-8360700)与 JEP 534 一致:64 位 HotSpot 默认启用紧凑对象头。临时关闭做对照时用:
text
-XX:-UseCompactObjectHeaders
官方原文是该开关 planned for deprecation and removal in a future release (计划在未来版本弃用并移除)。JEP 534 也写「intend to deprecate the old object header layout in a future release」。成稿口径固定为:计划弃用,不是 JDK 27 已经标 deprecated。早期邮件里「某版本 deprecate / 再 obsolete」的规划若没进正式 release notes,别写进正文。
堆/CPU「能省多少」不要写死成普适结论。JEP 534 Motivation 引用 SPECjbb2015 某一配置 约 22% 更少堆、约 8% 更少 CPU,另一配置更少 GC 次数;JSON 解析场景还有约 10% 耗时差异等数字。Inside.java 2026-09-28 重述这些数字,并强调结果高度依赖对象大小分布。Billy Korando 2026-09-12 文中「about 20%」是作者概括,不是硬 SLA。写法就按:官方基准某配置曾到约 22% 堆;你的服务以同负载实测为准。
单变量 A/B:钉死 GC,只差 ±UseCompactObjectHeaders
核心原则来自 Inside.java 2026-09-28 Closing Thoughts:
分别测新默认;一次改一个变量------例如
-XX:-UseCompactObjectHeaders,或选定的垃圾收集器。
可以这样拆:
| 组 | GC(显式钉死) | 紧凑对象头 | 用途 |
|---|---|---|---|
| A(JDK 27 默认头) | 例如 -XX:+UseG1GC |
默认开(可不写 +) |
主线上线候选 |
| B(关 COH) | 同一条 -XX:+UseG1GC |
-XX:-UseCompactObjectHeaders |
只回答「头布局」 |
| C(可选) | -XX:+UseSerialGC |
与 A 或 B 择一对齐 | 只回答「换 GC」,勿与关 COH 同改 |
要点:
- 可以只关 COH、不动 GC。 COH 与 GC 是独立开关;JEP 534 改的是对象头布局,不是「默认一律 G1」(那是 JEP 523,细节见前作)。
-XX:+UseSerialGC在 27 仍可用 (Inside.java 同文确认),但若你要看「旧 Serial 行为」,请单独开组,不要和±UseCompactObjectHeaders绑成一个 Diff。- 两组请显式写上同一收集器,别弄成「一组吃默认、一组写了 UseG1GC」这种隐性差异。

对照实验步骤(可复现)
下面命令只给骨架,别伪造输出;以本环境的日志与监控为准。
1. 固定版本与公共参数
bash
java -version
# 确认已是 27;GA 日口径:2026-09-15
两组共用同一套堆边界与容器配额,例如(数值按你们容量规划替换):
text
-XX:+UseG1GC
-Xms512m -Xmx512m
-XX:ActiveProcessorCount=1
2. A 组:默认 COH(开)
bash
java -XX:+UseG1GC -Xms512m -Xmx512m \
-XX:ActiveProcessorCount=1 \
-Xlog:gc*:file=gc-a-coh-on.log \
-jar your-app.jar
3. B 组:只关 COH
bash
java -XX:+UseG1GC -Xms512m -Xmx512m \
-XX:ActiveProcessorCount=1 \
-XX:-UseCompactObjectHeaders \
-Xlog:gc*:file=gc-b-coh-off.log \
-jar your-app.jar
4. 可选 C 组:只换 Serial(COH 状态与 A 或 B 对齐其一)
bash
java -XX:+UseSerialGC -Xms512m -Xmx512m \
-XX:ActiveProcessorCount=1 \
-Xlog:gc*:file=gc-c-serial.log \
-jar your-app.jar
若 C 组要回答「在默认开 COH 时 Serial 怎样」,就不要加 -XX:-UseCompactObjectHeaders;若要回答「旧头 + Serial」,再单独加关 COH------但那是第三轴,报告里分开写。
5. 负载与窗口
- 同一压测脚本、同一预热时长、同一采样窗口。
- 变更窗口不要叠发布、不要叠扩缩容。
- 需要看启动成本时,单独记 cold start;不要和稳态 RSS 混在一张表里。

指标清单:看什么、别急着下结论
建议同一张表只填「可归因到头布局」的列:
| 指标 | 为何看 | 注意 |
|---|---|---|
| 进程 RSS / 容器 memory working set | COH 直接影响对象头占用 | 区分堆内占用与堆外、映射文件 |
| 堆使用高水位 / 直方图 | 官方基准谈的是 heap 侧收益 | 勿直接把「约 22%」填进容量工单 |
| GC 停顿 P99 / 并发 GC CPU | 头变小可能改变分配与回收节奏 | 与「换了 G1」的停顿模型分开看 |
| 启动时间到就绪 | 类加载 + CDS + 堆初始化 | 自定义 CDS 未按开关重做时会污染结果 |
| 业务 CPU | JEP 引用过某配置约 8% CPU | 仍以你们服务剖析为准 |
Inside.java 把关 COH 明确写成 upgrade 期间有用的 A/B comparison。若 B 组明显更差且无兼容问题,优先保留默认开;若某条依赖旧头布局的本地代码/JNI 异常,再用关 COH 做临时对照,并跟着「计划弃用」时间表走,别当成长期默认。
CDS:on/off 各有档案,别写死文件名
切换 ±UseCompactObjectHeaders 会改用对应的 CDS 档案;自定义 -Xshare:dump 必须按目标开关重做 。EA release notes 与实现 PR(JDK-8360700)对默认/opt-out 的 *.jsa 命名口径不完全一致 ,正文不要写死某一个如 classes_coh.jsa 当作唯一正确答案。
实操:
bash
ls $JAVA_HOME/lib/server/*.jsa
按本机/镜像里实际存在的文件名核对;A/B 两组若依赖 AppCDS,请分别在「COH 开」「COH 关」目标参数下 dump,再加载,避免一组命中共享档案、另一组静默回退。小容器文里的 GC 默认与堆缩放清单,和本篇的 CDS 注意点互补,不要把两篇合成一次「全量默认值扫盲」。
升级窗口里怎么排期
建议把对照嵌进发布流水线,别临时手敲:
- 金丝雀 A:JDK 27 默认(G1 + COH 开),观察一轮完整业务日或压测窗口。
- 金丝雀 B :同镜像构建产物,仅启动参数多
-XX:-UseCompactObjectHeaders,GC/堆/配额与 A 一致。 - 决策 :若 B 无兼容收益且指标不优于 A,生产保留默认开,并删掉关开关;若 B 暴露 JNI/本地假设问题,把关开关当临时豁免,同时跟进官方「计划弃用」时间表。
- 再测 GC(可选):仅在需要回答「要不要 Serial」时开 C 组;报告标题写清「GC 轴」,避免和 COH 轴混读。
同一份编排模板里用两个 Deployment / 两个 JVM_OPTS 片段切换即可,比「改完默认再猜」更好回滚。监控面板为 A/B 打同一套标签(coh=on|off),事后用同一查询对比 RSS 与延迟分位。
和「小容器默认值」文怎么配合读
| 问题 | 去哪篇 |
|---|---|
| 未指定 GC 是否一律 G1?旧 Serial 启发式?Min/MaxHeapFreeRatio? | 167277840《JDK27升级后小容器GC和内存默认值变了》 |
| 升完后如何证明「慢/省」来自 COH 而不是 G1? | 本篇:单变量 ±UseCompactObjectHeaders |
| 虚拟线程下瓶颈是否已从线程数挪走? | 166887829《Java 虚拟线程瓶颈转移》 |
升级清单若要「默认值 + A/B」一起做:先按前作钉死 GC 意图与堆缩放预期,再按本篇加一轮只关 COH 的对照;两轮报告分开归档。
FAQ
Q:关掉紧凑对象头用哪条 JVM 参数?
A:-XX:-UseCompactObjectHeaders。它计划在未来版本弃用并移除(planned for deprecation and removal),不是「JDK 27 已标 deprecated」。长期方案应适应默认开,而不是把关开关写进所有镜像。
Q:A/B 时一次改几个变量?
A:一次一个。 推荐两组都显式 -XX:+UseG1GC(或都 Serial),只差 ±UseCompactObjectHeaders。版本升级、堆大小、CDS、GC 不要和头布局捆在同一 Diff 里。
Q:和 G1 默认切换(JEP 523)怎么拆开测?
A:COH 与「未指定则 G1」是两条线。看头布局 → 钉死同一 GC,只拨 COH;看旧 Serial → 另开 +UseSerialGC 组,并保持 COH 状态与对照轴一致。Inside.java 2026-09-28 明确把「关 COH」和「选 GC」列成两个可分开改的变量。
Q:能不能直接按「省堆约 20%」做容量规划?
A:不能。JEP 534 写的是 SPECjbb2015 某配置约 22% 堆等结果;对象大小分布不同,收益会变。以你们同负载的 RSS/堆水位实测为准。
小结
JDK 27(2026-09-15 GA)默认打开紧凑对象头;临时对照用 -XX:-UseCompactObjectHeaders,口径是 计划弃用 ,不是已经弃用。A/B 的价值不在复述默认值清单,而在 钉死同一 GC,只差头布局 ,并分开测换 GC。数字引用官方基准时带上「某配置 / JEP 534」,CDS 按本机 *.jsa 实核、按开关重 dump。先跑通这一轮单变量对照,再决定生产镜像是否保留默认 COH,比一次改完再猜原因省事。