面向 P6-P7+ 工程师 问题推导 + 体系架构 · 长文 约 9,000–11,000 字 信息截止 2026-08

JVM 内存模型与垃圾回收选型:
从 STW 约束到 G1 / ZGC 的业务决策

这不是背诵各代 GC 名词的入门课,而是给已经上过线、被停顿与内存问题打过的工程师:弄清内存区域职责、理解 G1/ZGC 各自换取了什么,并在吞吐与延迟之间做出可辩护的选型。

主线风格:问题推导 + 体系架构 版本假设:JDK 17 / 21 LTS 证据等级:官方文档优先,标注推断与生产经验
问题现场 · 复合场景

1同一套业务,为什么有人要杀 STW,有人却嫌 ZGC「更慢」

复合场景 · 综合多起线上调优讨论,非单一事故TYPICAL SCENARIO

交易链路服务在大促期间 p99 从 80ms 被拉到 300ms 以上,排查后发现并非业务锁竞争,而是偶发较长 STW。平台组提议切换 ZGC。与此同时,离线报表集群把收集器改到低延迟方向后,作业总时长明显变长——CPU 被并发标记吃满,吞吐掉了一截。

两边都按所谓最佳实践换了新 GC,结果一个变好、一个变差。真正的问题不是哪个收集器最新,而是:你的 SLA 买的是尾延迟,还是单位时间完成的工作量

把这个复合场景再拆一层:交易链路的失败形态是网关超时与熔断,单次 STW 200~400 毫秒就足以打穿超时预算;报表集群的失败形态是作业拖堂、占满队列,CPU 被并发标记吃掉后,整体吞吐反而下降。两边若共用同一套「默认低延迟」模板,等于用一张药方治两种病。

本文按这条冲突展开:先把内存模型讲成可推理的地图,再比较 G1 与 ZGC 的约束与代价,最后给出可落地的决策矩阵。容器化参数与 cgroup 坑留给系列中的 T10,本文只在边界处点明联动。面向已经上过线、被停顿与内存问题打过的工程师,目标是形成可辩护的选型判断,而不是背诵名词。

还可以把冲突翻译成三个可验证问题。第一,延迟尖刺是否与 GC 暂停时间对齐?若尖刺来自锁、IO 或下游,换收集器只会浪费窗口。第二,系统的成功标准是尾延迟还是作业完成时间?在线服务与批处理给出的答案往往相反。第三,你是否付得起并发回收的 CPU?在已经被限流的容器里,低延迟收集器可能把「暂停短」换成「处处慢」。三个问题都答得清楚,选型会议才会从立场之争变成证据之争。

团队协作上也常见一种误区:应用研发只关心业务 RT,平台组只关心 GC 暂停百分位,容量组只关心机器成本。三者割裂时,最容易出现「暂停确实降了,但副本数和 CPU 预算被动上调」的隐性成本。因此本文强调可辩护:任何收集器切换都要能同时解释延迟、CPU、堆和回滚策略。

分享结构如下:第二章把运行时数据区画成可推理地图;第三章讲分配路径与晋升;第四、五章分别拆 G1 的 Region 规划与 ZGC 的屏障代价;第六、七章做对照与适合边界;第八章给观测与发布纪律;第九章收束为决策矩阵。机制描述属于机制共识,场景数字属于数量级经验,上线前必须用本机 workload 复测。版本假设以 JDK 17 LTS 为默认基线,JDK 21 LTS 在分代 ZGC 等处单独标注。

常见误区

「GC 越新越好」「堆越大越安全」——两者都会在错误负载形态下放大事故窗口。堆过大可能让单次标记与转移更贵;新收集器可能用 CPU 换延迟。

体系架构 · 内存地图

2运行时数据区:先分清 GC 管什么、不管什么

JVM 规范定义了若干运行时数据区。工程讨论里最容易混淆的是:人们说「内存爆了」时,究竟是堆、元空间、直接内存,还是原生线程栈。只有把边界画清,GC 选型才有意义——因为很多 OOM 根本不在堆上,换收集器无济于事。

图 1 · JVM 运行时数据区地图(简化)
自制示意图
线程私有 PC Register 当前字节码位置 JVM Stack 栈帧 / 局部变量 Native Method Stack JNI 调用 堆 Heap(GC 主战场) Young / Eden + Survivor 对象分配热点 · Minor GC Old Generation 长寿对象 · Mixed/Full GC 成本中心 非堆 Metaspace 类元数据 · 本地内存 Code Cache JIT 编译代码 Direct Memory
关键点:GC 讨论的是堆;OOM 却可能发生在 Metaspace / DirectMemory / 原生线程栈。选型前先分清哪块内存在涨。
区域主要职责典型故障形态
堆 Heap实例对象、数组Java heap space;频繁 Full/Mixed GC
Metaspace类元数据(本地内存)Metaspace OOM;动态代理或热部署泄漏
JVM Stack栈帧、局部变量、操作数栈StackOverflowError;线程过多导致原生内存上涨
Direct MemoryNIO 堆外缓冲堆外 OOM;Netty 等组件未释放
来源:Java Virtual Machine Specification · Runtime Data Areas(JVMS §2.5)。
已核验事实

Metaspace 使用本地内存而非永久代堆空间(Java 8 起)。堆参数调好了,并不能自动约束类元数据与直接内存。

一张实用检查表:看到 OOM 先读完整异常文案;看到「内存涨」先看容器 RSS 还是堆占用;看到「延迟尖刺」再去对齐 GC 暂停日志。这条分流能避免把所有问题都塞进收集器讨论。

再补一层组织语言。很多事故复盘写成「JVM 内存问题」,其实至少要拆成四类:堆内存活集过大、分配速率过高、非堆资源泄漏、以及调度与限流导致的 CPU 不足。前两类才与 GC 算法强相关;后两类换 G1 或 ZGC 常常无效。把复盘标题写精确,是选型能力的一部分。

从规范角度看,运行时数据区的划分是机制共识;具体实现里 HotSpot 还有 TLAB、卡表、记忆集、屏障等工程细节。本文不展开源码级实现,但会在需要做决策的地方指出:哪些是规范保证,哪些是 HotSpot 常见行为,哪些只是经验法则。避免把某一版本实现细节误当成跨版本承诺。

若你维护的是多服务平台,建议在内部知识库固定一张「内存区域—信号—工具」对照:堆用 GC 日志与 heap dump,Metaspace 看类加载与 jcmd VM.metaspace,直接内存看 NIO/Netty 指标,线程看线程数与原生内存追踪。有了这张表,值班同学不会在凌晨把所有告警都丢给「换 ZGC」。

堆在逻辑上仍区分年轻代与老年代,但 G1 / ZGC 会用 Region 或页面等结构重新组织物理布局。年轻与年老是代际假设,Region 是实现结构,二者不要混为一谈。可以用仓库类比:年轻代是当日周转区,老年代是长期寄存区——周转区清理频高、单次快,寄存区清理一次代价高,这正是在线服务更怕老年代毛刺的原因。

元空间膨胀常见于 Spring 动态代理、脚本引擎、频繁热加载。表现是进程 RSS 上涨而堆使用率不高。可用 -XX:MaxMetaspaceSize 设上限,但设得过小会在类加载高峰触发 Metaspace OOM。直接内存则是 Netty、gRPC、NIO 的常客:堆内对象回收了,DirectByteBuffer 仍占 Native;容器 limit 通常包含这部分。「堆还有三成空闲却被 OOMKilled」在生产复合场景并不少见,细节见系列 T10,本文只强调选型必须把 Native 算进总账。

问题推导 · 分配路径

3对象从哪里来:TLAB、Eden 与逃逸分析

GC 压力首先来自分配速率。理解分配路径,才能解释为什么有的服务年轻代回收很勤但仍稳定,有的稍微抖动就晋升爆炸。选型之前,先问:每秒创造了多少字节的垃圾?对象平均寿命多长?

图 2 · 对象分配路径:TLAB → Eden → 晋升
自制示意图
new Object 应用线程 TLAB 快分配 无锁 bump pointer Eden Minor GC 清理 Survivor / Old 年龄阈值后晋升 逃逸分析可能栈上分配——那是编译器路径,不是 GC 算法承诺
关键点:多数短命对象死在 Eden;GC 选型要先看对象寿命分布与分配速率。

3.1 TLAB:用线程本地缓冲换分配吞吐

热点路径上,线程先在自己的 TLAB 里 bump-the-pointer 分配,避免对堆全局分配锁的竞争。TLAB 用尽再向 Eden 申请新的缓冲。这是吞吐的基础机制,而不是某个收集器的专利。G1 与 ZGC 都建立在「分配很快、回收另算」的前提上。

3.2 逃逸分析:少进堆是编译器的事

若对象不逃逸出方法,JIT 可能栈上分配或标量替换,从而降低分配速率、间接减轻 GC。但它不是可以在配置里打开就保证生效的开关式承诺——依赖代码形态与编译阈值。把它写成容量规划硬假设,属于过度承诺。

作者解释

逃逸分析适合作为性能优化的理解模型,不适合作为「我们堆可以开小一点」的唯一依据。没有分配剖析证据时,仍按最坏合理分配速率做预算。

延伸阅读:Java SE 21 GC Tuning Guide(docs.oracle.com/.../gctuning)。

实践上,可用异步剖析或 JFR 观察分配热点:如果少数调用路径贡献了绝大多数分配,先做代码层复用与对象池化评估(谨慎),再谈换收集器。否则你会在错误层优化。

对象池化尤其需要克制。它在序列化缓冲、短生命周期游戏对象等场景可能有效,但在业务服务里滥用对象池,经常把分配问题转换成池污染、错误复用与隐蔽并发缺陷。更稳妥的顺序是:先减少不必要的中间对象与装箱,再考虑池化,最后才是收集器层的手段。

晋升爆炸是另一个分配侧信号。如果年轻代回收很频繁,同时老年代稳步上涨,说明有相当比例对象「活过了」年轻代窗口。这时要区分:业务缓存是否过大、会话是否堆积、是否存在监听器未注销。把这些当成 GC 参数问题去拧 IHOP,往往只是推迟 Full GC。

对延迟敏感系统,还要看分配是否突发。流量尖峰会在数秒内把 Eden 打满,即使平均分配速率看起来温和。压测必须以生产峰值形状为准,用平稳 QPS 压测得出的 GC 结论,不足以支持大促切换决策。

可达性决定对象是否会被回收。GC Roots 包括虚拟机栈中的引用、静态字段、JNI 引用等;从 Roots 出发不可达的对象即为垃圾。常见泄漏模式是静态无界缓存、监听器未注销、线程池场景下 ThreadLocal 未 remove——对象在逻辑上不用了,但引用链仍可达,老年代占用持续攀升。这类问题换 ZGC 不能根治,只能改变停顿形态。

分配速率是选型里常被忽略的变量:单位时间新建对象的字节数。高 QPS 接口若每层都做深拷贝与大 JSON 解析,会推高 Young GC 频率;同时若有大量长生命周期缓存,晋升速率上升又会加重老年代压力。架构评审需要有人能把现象翻译成「分配太快」还是「回收跟不上」——优化分配往往比换 GC 更便宜。

机制拆解 · G1

4G1:把停顿目标变成 Region 回收的规划问题

G1 将堆划分为等大 Region,跟踪每个 Region 的回收收益,在停顿目标约束下优先回收垃圾比例高的区域。它是 JDK 9 起的服务端默认收集器,也是大多数业务服务今天的合理起点。理解 G1,关键不是背参数列表,而是理解它把「停顿」当成可规划资源。

图 3 · G1:Region 化堆与 Mixed GC 回收集合
自制示意图
堆被切成等大 Region:Eden / Survivor / Old / HumongousEESOOEHOEOOEOSOEOOEOEdenSurvivorOldHumongousMixed GC:优先回收垃圾多的 Old Region
关键点:G1 用可预测停顿目标指导回收哪些 Region,而不是清空整个代。
关键参数作用实践提醒
-XX:MaxGCPauseMillis停顿目标(软目标)过小会迫使更频繁回收,可能损吞吐;不是硬实时保证
-XX:InitiatingHeapOccupancyPercent触发并发标记的堆占用阈值过高可能来不及在 Full GC 前回收;过低增加并发周期
Humongous 对象大对象直接占 Humongous Region大数组与大缓冲会导致碎片,需从分配侧治理
来源:Oracle GC Tuning Guide · G1(G1 Garbage Collector)。
生产实践

先用默认 G1 加合理堆(结合容器内存,见 T10),用 GC 日志确认 Mixed GC 是否跟上分配;再调 IHOP 与停顿目标。避免一上来把停顿目标打到不现实的数值。

当 Mixed GC 长期跟不上晋升速度,G1 可能退化到更昂贵的回收路径。这时优先查:是否堆偏小、是否存在泄漏、是否 Humongous 过多,而不是立刻宣布「G1 不行,上 ZGC」。

关于停顿目标,一个常见误解是把 MaxGCPauseMillis=50 理解成硬实时。G1 会尽量朝目标努力,但在分配失控、堆碎片、标记工作过重时,仍可能出现更长暂停。因此目标应来自业务预算的一部分,而不是「越小越专业」的竞赛。

Region 大小与大对象策略也会影响体感。当应用频繁分配接近 Region 体量的大数组时,Humongous 路径会让老年代与碎片问题提前到来。治理手段优先是拆分缓冲、流式处理、避免一次性加载超大结构;参数层面的补救往往有限。

如果你从 Parallel 或 CMS 年代迁到 G1,不要指望「参数平移」。更有效的迁移方式是:先默认,再根据日志里的暂停与并发标记周期做小步调整,并保留切换前一周的基线指标。大爆炸式抄别人的参数单,是生产环境里的高频反模式。

G1 的优势还在于生态与可运维性:资料多、案例多、大多数监控系统对 G1 日志字段更熟悉。对平台团队而言,默认收集器的可支持性本身就是架构收益——除非延迟证据足够强,否则不要轻易放弃这个收益。

一次典型周期可粗分为:Young GC(回收 Eden / Survivor,STW 拷贝存活对象)、并发标记(与应用并行,SATB 记录引用变化)、Remark(短 STW 修正)、Cleanup(选择回收候选 Region)、Mixed GC(回收部分老年代 Region 加年轻代)。当 InitiatingHeapOccupancyPercent(IHOP,默认约百分之四十五)触发并发标记时,若应用写入速度超过并发标记能力,可能出现并发周期跟不上分配——表现为频繁 Mixed GC 或 evacuation failure,日志中可见 To-space exhausted 等关键字。

开篇交易链路抖动的常见成因之一,正是老年代存活对象多、分配速率极高时,Mixed GC 压不住停顿。经验顺序是:先把 Mixed GC 与应用 p99 曲线叠图,再决定调 IHOP、增堆,还是换收集器——避免只把 MaxGCPauseMillis 改成五十却无视老年代存活集。并行收集器与串行收集器在 JDK 十七及以上仍存在,但新项目默认讨论 G1 与 ZGC 即可;纯批处理、停顿不敏感的历史系统迁移 LTS 时需单独评估。

机制拆解 · ZGC

5ZGC:用并发与屏障换尾延迟

ZGC 的设计目标是低延迟:把标记与转移尽量并发化,并将停顿控制在很短的时间窗口(具体数值随 JDK 版本演进,请以发行说明为准)。它通过着色指针与读屏障等机制,让应用线程在访问对象时协助维持堆的一致性视图。

图 4 · ZGC:并发标记/转移与着色指针(概念)
自制示意图 · 简化
应用线程 几乎持续运行 读屏障 看到正确对象视图 ZGC 并发阶段 Mark → Relocate 停顿目标:亚毫秒级(版本相关) 着色指针编码对象状态 代价 更高 CPU / 内存开销 对吞吐型批处理未必更优 需要压测证据
关键点:ZGC 优化的是尾延迟,不是一切场景更快。用延迟 SLA 决定是否付 CPU 税。

更可能受益

  • 在线交易、网关、查询:p99 或 p999 硬约束
  • 较大堆且能接受一定 CPU 开销
  • 团队具备 GC 日志与压测对比能力

更可能不合适

  • 纯吞吐批处理、CPU 已打满
  • 极小堆或短生命周期工具进程
  • 把换 ZGC 当唯一优化,无视分配与泄漏
已核验事实

ZGC 在 JDK 15 成为生产特性,并在后续 LTS 持续增强(如分代 ZGC 等,以你使用的 JDK 发行说明为准)。选型时写明 JDK 大版本,避免跨版本口头经验误用。

来源:OpenJDK ZGC(wiki.openjdk.org/display/zgc)。

JDK 21 上评估 ZGC 时,请同时阅读发行说明中与分代 ZGC、平台支持相关的条目。文档版本与生产运行时版本不一致,是低延迟优化里最常见的「正确的话用在错误版本」问题。

从机制直觉上,可以把 ZGC 理解成:它更愿意在应用运行期间持续做回收工作,从而避免把工作堆积成长暂停。这必然占用 CPU 与内存带宽。若你的服务已经在成本线边缘运行,低延迟能力可能要以更多副本或更大规格换取——这笔账要在选型文档里写明,而不是只展示暂停曲线。

另一个现实约束是排障熟练度。团队如果只会看 G1 的 Mixed GC 叙事,突然切到 ZGC 后面对新的日志语义会失明。切换前至少完成:日志字段解读演练、一次失败回滚演练、一套对比看板。否则「机制上更强」会变成「组织上更脆」。

对多租户平台,还要避免「一刀切全局 ZGC」。让延迟敏感服务单独评估,批处理与异步消费者保留 G1,往往比全公司统一收集器更合理。统一的是观测与发布规范,不是算法本身。

机制上,染色指针在六十四位指针高位编码 Marked / Remapped 等状态,减少额外数据结构遍历;读屏障在应用线程加载引用时,若发现指针颜色与当前周期不一致则触发修正,保证并发移动后引用仍有效。读屏障带来一定 CPU 开销,换取极低 STW。对象压缩与移动主要在并发阶段完成,STW 仅做极短安全点同步。

JDK 十七与二十一的差异要写进评审纪要:十七起 ZGC 在主流平台为生产特性,但是单代模型,极高分配速率下全堆扫描与 CPU 开销可能更明显;二十一引入分代 ZGC(JEP 四百三十九),重新利用代际假设降低短命对象主导场景的成本,在线服务通常更值得评估。启用形态以发行说明为准,例如二十一上 -XX:+UseZGC -XX:+ZGenerational。虚拟线程与 GC 正交,但可能改变线程栈与分配模式,升级后应重新压测。平台支持矩阵(含 ARM64)随小版本变化,「开发机正常」推不出「生产同样行为」。

何时不必上 ZGC:堆明显偏小、停顿要求宽松、团队零运维经验且没有灰度窗口。此时把 G1 日志读懂、把混部拆开,收益往往大于冒险切换。Shenandoah 与 ZGC 同属停顿可控谱系,实现路径不同;若团队已标准化 OpenJDK 十七或二十一,通常先在 G1 与 ZGC 间决策即可。

框架训练 · 对照

6G1 vs ZGC:一张表说清楚交换条件

维度G1ZGC
优化目标平衡吞吐与可控停顿极力压低停顿
默认友好度服务端默认,资料与案例多需主动开启与验证
CPU 成本相对更省(常见负载)并发阶段吃 CPU 更明显
调优重心停顿目标、IHOP、大对象堆大小、分配、CPU 余量、版本能力
失败模式Mixed 跟不上导致退化资源不足时吞吐塌陷或分配停滞

声明:上表是机制与工程经验层面的对照,不是某一基准测试的结果。任何「我们换了之后快了百分之三十」都必须带工作负载说明,否则不可外推。

做对照实验时,至少固定这些变量:JDK 小版本、堆大小、容器 CPU 与内存 limit、压测脚本、预热时间、成功条件。只改收集器开关,才能谈因果。若同时改了堆、副本数和代码,得到的是故事,不是结论。

汇报结果时建议同时给出:暂停时间分布、应用 p99、CPU 利用率、GC 吞吐占用、错误率与超时率。只展示「最大暂停从 120ms 降到 2ms」却隐藏 CPU 上升与超时率变化,属于选择性叙事,不适合作为架构决策依据。

若对照显示 ZGC 在 p99 上获胜,但平均 CPU 上升导致需要扩容百分之二十,要用成本模型换算:延迟收益值不值这笔钱。有的业务值,有的业务不值——这正是决策矩阵存在的理由。

再对齐四个术语,避免评审各说各话。STW 是为保证一致性暂停全部应用线程;吞吐量是应用运行时间占总时间的比例;延迟是单次请求耗时分布,在线 API 关心 p99 与 p999;Footprint 是进程常驻内存,ZGC 并发 worker、元空间与直接内存都会计入。吞吐与延迟不是同一轴上的越高越好——G1 用停顿目标做软约束,ZGC 明显偏向延迟轴,选型本质是声明更能接受哪一个轴上的损失。

场景对照可用经验表辅助讨论(非基准排名):支付与订单 API 在四到十六吉字节堆上,可评估二十一的分代 ZGC 或严格隔离后的 G1;网关与 BFF 常以 G1 为起点;日终对账与 ETL 优先 G1 吞吐调优;大堆缓存型嵌入式 Java 可试 ZGC;消息消费在 rebalance 成本高时试低停顿,但 CPU 已饱和时读屏障可能恶化 lag。读表时记住:推荐起点是默认选项,代价列是刻意写出的权衡。

适合 / 不适合

7什么负载该保守,什么负载该激进

7.1 适合把延迟当成一等公民的场景

  • 同步调用链长,尾延迟会放大到用户可感知
  • 有明确的 p99 预算,且 GC 已被证明占预算显著比例
  • 机器 CPU 有余量,能为并发 GC 付费

7.2 不适合一上来上 ZGC 的场景

  • 批处理、ETL、报表:完成时间比单次停顿更重要
  • 尚未治理内存泄漏或巨型对象,却幻想换收集器吸收问题
  • 容器 limit 与堆比例未对齐(先看 T10)
踩坑

在 CPU throttle 的容器里启用高并发低延迟收集器,可能出现停顿短了但请求更慢——因为应用线程与 GC 线程抢同一颗被限速的 CPU。

适合与不适合还随时间变化。服务早期流量小、堆小,G1 足够;半年后缓存变大、存活集上升,可能重新具备评估 ZGC 的条件。反之,若业务从同步链路改成异步削峰,延迟约束变弱,也可以从 ZGC 退回 G1 以节省 CPU。把收集器选择写成一次性终身决定,不符合演进现实。

对数据类服务(本地缓存很大、对象图复杂)要额外小心:存活集大时,任何收集器都更贵。此时架构手段——分级缓存、离堆缓存、缩小会话——往往比收集器切换更有效。GC 是最后一公里,不是第一公里。

7.3 新手常见误区对照

误区正解角色侧重
GC 越新越好,生产一律上 ZGC按 SLA 与堆规模选型;批处理吞吐场景 G1 往往更省心架构 / 开发
堆越大越安全堆过大可能放大回收成本;应分析存活对象与泄漏运维 / 开发
只调堆大小不改隔离策略延迟问题需联动 GC 类型、混部隔离与分配优化架构
把一切 STW 当成故障Young GC 短暂停通常正常;关注 Mixed / Full 与 p99 对齐开发
十七与二十一参数完全通用分代 ZGC 等开关需查发行说明;升级后必须回归运维
线上改 GC 无需灰度应灰度、可回滚,并保留日志与剖析基线运维 / SRE

混合部署用「折中参数」掩盖冲突,是开篇复合场景的典型反模式。吞吐与延迟冲突时,正确解是拆开:不同 Pod、节点池或资源队列,而不是把停顿目标调到一个谁都不疼的假象值。

生产实践

8上线前最低限度的观测与参数纪律

  1. 打开统一 GC 日志,保留切换前后对比窗口。
  2. 明确堆大小来源:裸机 -Xmx 或容器 MaxRAMPercentage(见 T10)。
  3. 用业务压测而不是微基准:对照 p99、CPU、GC 暂停、分配速率。
  4. 建立回归门槛:切换收集器必须可回滚。
# JDK 17/21 示例:G1 与 ZGC
java -Xms2g -Xmx2g -XX:+UseG1GC -Xlog:gc*:file=gc-g1.log:time,uptime,level,tags
java -Xms2g -Xmx2g -XX:+UseZGC -Xlog:gc*:file=gc-zgc.log:time,uptime,level,tags
生产实践清单

① 先证明 GC 是延迟瓶颈;② 保留 G1 baseline;③ 同负载对比 ZGC;④ 观察 CPU 与分配;⑤ 文档化版本与参数;⑥ 容器环境先做内存预算。

日志里至少看清:暂停时间分布、并发周期是否频繁、是否出现 to-space exhaustion 或分配失败、老年代占用趋势是否单调上涨。单调上涨优先怀疑泄漏,而不是收集器「不够强」。

发布纪律建议写成平台规范:变更单必须附 JDK 版本、收集器、关键参数、回滚版本、观察指标与观察时长;灰度从非关键实例开始;大促前禁止无回滚窗口的收集器切换。这些听起来像流程,实际上是在保护你在压力下不做不可逆实验。

观测方面,除了 GC 日志,还应把暂停指标纳入与业务 p99 同一看板。只有当两者时间对齐,讨论才有证据。若公司已有统一剖析平台,把分配火焰图作为切换前后的必选附件,能显著减少「我觉得是 GC」的口水战。

参数文档化也很关键。很多故障来自镜像里偷偷加的实验参数,人走了之后没人敢删。镜像构建应使 JVM 参数可追溯:来自哪份配置、谁批准、对应哪次压测报告。没有出处的参数,默认视为技术债。

8.1 四步评审清单

  1. 写清 SLA 数字与失败代价(超时熔断、对账延误、客诉)。
  2. 画部署拓扑,标出混部与共享节点。
  3. 采集至少含高峰的二十四小时 GC 日志与延迟分位。
  4. 预发做对照实验,控制变量仅改 GC 与堆——这一步常被跳过,导致预发很快、生产仍抖。

服务分级规范可以把默认安全值写死:例如核心交易链路默认 JDK 二十一加分代 ZGC,一般 CRUD 微服务默认 G1,离线任务单独资源池。偏离默认值需要书面理由与压测报告。回到开篇复合场景,可落地的治理组合通常是调度层迁出批处理、API 池试点 ZGC 或精细调 G1、观测层把暂停与超时链路关联——三套动作并行,而不是只改一个参数。

决策框架 · 收尾

9决策矩阵:今天怎么选,下一步验证什么

图 5 · GC 选型决策流(工程简化)
决策框架
延迟敏感?p99/p999 有硬约束? 评估 ZGC(JDK17/21) 默认优先 G1 验证分配速率、堆大小、CPU 余量 压测对比 G1 baseline 调 MaxGCPauseMillis / 堆占用 观察 Mixed GC 与 Humongous 容器环境:先对齐 cgroup 与堆占比,再谈 GC(见 T10)
关键点:先 SLA 与负载形态,再选收集器;用压测证据替换听说 ZGC 更好。

9.1 决策矩阵

你的情况继续 / 选择 G1评估 ZGC先别改收集器
通用在线服务,p99 可接受优先无强动机
尾延迟撞 SLA,GC 日志证实作 baseline值得压测
批处理吞吐优先通常更合适慎用
怀疑泄漏或巨对象不能当解药先做堆分析(T09)
K8s 内存参数未对齐暂缓先做 T10 预算

9.2 相邻知识地图

  • T09:CPU 飙升、泄漏、OOM、死锁的工具链
  • T10:容器化下的堆与元空间、cgroup 与 OOMKilled
  • JIT、逃逸分析与分配剖析
阶段性结论

GC 选型是约束满足问题:在延迟、吞吐、CPU、工程复杂度之间找可运维的点。默认 G1 仍然是多数业务的理性起点;ZGC 是被延迟证明过之后的武器,不是身份象征。

9.3 已知 / 未知 / 下一步

类别内容
已知分代与 Region、并发收集的基本机制;G1 为常见默认;ZGC 面向低延迟
因负载而异切换收益、合适堆大小、CPU 余量
下一步验证用生产流量回放或压测做 G1 与 ZGC 对比,并输出暂停分布与 CPU 对比

最后回到开头的复合场景。交易链路若已证明暂停占 p99 预算,且 CPU 有余量,ZGC 值得进入正式压测;报表集群若以完成时间为核心指标,应坚持吞吐友好路径,并把「低延迟崇拜」挡在架构评审之外。同一家公司、同一天、两套相反结论——这不是矛盾,而是约束不同。

若你读完只带走一句话,那就应该是:先用证据定义问题,再用收集器匹配约束,最后用钱与复杂度验收方案。工具会演进,这套判断顺序不太会过时。

动手练习建议:在本地用 JDK 十七与二十一各启动一份相同应用,分别指定 G1 与 ZGC,打开 GC 日志,用固定并发压十分钟,记录年轻代与混合回收次数以及 p99。无需追求精确数值,目标是能口述两种日志长什么样、对应哪种设计取舍——这比背诵参数列表更接近生产选型能力。下一步可衔接 T09 工具链排障与 T10 容器内存预算,把「会选」推进到「会查」与「会配」。

附录 · 评审话术与案例边界

10架构评审里怎么把 GC 讨论谈完

很多评审会议在收集器名词上打转,一小时后没有可执行决议。更有效的议程是固定五段:现象与时间线、证据(日志与指标)、约束(延迟、吞吐、成本)、方案对照(含不改收集器的方案)、验证与回滚。没有证据的段落不允许进入方案对照;没有回滚的方案不允许进入发布窗口。

下面给出三份可直接改写进评审纪要的「边界声明」。它们不是官样文章,而是用来防止结论被断章取义。

10.1 边界声明模板

  • 版本边界:结论仅适用于 JDK 17 或 21 的具体发行版;不得外推到未验证的发行版。
  • 负载边界:结论绑定某类流量模型(读写比、对象大小、峰值形状);流量形态变化需重做对照。
  • 成本边界:若低延迟方案要求扩容,必须在决议中写明预算来源,避免技术决议偷渡资源决议。

10.2 复合场景延伸:两种「看起来像 GC」的假象

第一类假象是锁竞争。线程在安全点附近排队,监控上像长暂停,实则是应用锁或偏向锁撤销等问题。区分手段是同时看线程转储与 GC 暂停是否严格重合。第二类假象是下游超时重试放大。重试风暴抬高分配与 CPU,GC 指标变差只是结果。若只盯收集器,会永远在症状层打转。

第三类假象更隐蔽:原生内存上涨触发了容器 OOMKilled,业务侧只看到重启,有人却在讨论 Full GC。此类问题应转入 T10 的 cgroup 与堆外预算,而不是继续加堆。能把问题正确转科,本身就是架构判断力。

10.3 与系列文章的接口

本文负责「选哪类收集器、如何证明」。T09 负责「已经出故障时如何取证」。T10 负责「容器里内存数字为什么对不上」。三者应被当成一条证据链:T10 保证测量单位正确,T08 保证选型逻辑正确,T09 保证事故可复盘。分开阅读可以,分开决策容易漏约束。

若你的组织正在做运行时标准化,建议沉淀三样资产:默认 JVM 参数基线(按服务类型分档)、收集器切换实验手册、以及一份「禁止事项」——例如大促前一周禁止无基线对照的收集器变更、禁止在未开启 GC 日志时发布 JVM 参数、禁止把实验参数写死在业务镜像却不登记。标准化的目标不是剥夺灵活性,而是让灵活性可审计。

对新人培养,也可以用本文做一次研讨课:给出两份匿名 GC 日志与一份业务 p99 曲线,让学员判断该不该上 ZGC。研讨的评分标准不应是「是否选出时髦方案」,而是「是否正确识别约束与证据缺口」。能说出「证据不足,保持 G1 并补充剖析」的学员,往往比只会背参数的人更可靠。

生产实践

把 GC 选型写入服务等级目标讨论:若 SLO 只有平均值没有尾延迟,就不要用尾延迟收集器去「超额完成」一个未定义的目标。目标未定义时,最优策略是保守与可运维。

信息截止于 2026 年 8 月。若你在更新的 JDK 版本上看到新的默认行为或新的分代实现细节,请以发行说明与官方调优指南为准,并重新跑对照实验。机制直觉可以复用,数字结论必须重测。

10.4 一份可复制的验证清单

切换前:确认 JDK 版本一致;确认 GC 日志已开启且可采集;确认有 G1 基线至少覆盖一个完整业务高峰;确认容器 CPU 与内存 limit;确认回滚镜像与参数;确认观察看板包含暂停、p99、CPU、错误率。切换中:灰度比例与观察窗口写进变更单;出现错误率或 CPU 异常立即回滚,不要「再观察一晚」。切换后:保留对照报告;更新服务默认模板;把结论同步给容量与值班文档。

验证清单看起来像项目管理,但它解决的是工程上最贵的失败模式:人们在压力下凭记忆操作。把记忆变成清单,GC 选型才能从个人经验升级为组织能力。

也可以把清单做成门禁脚本的一部分:发布系统检查启动命令是否包含 GC 日志参数、是否登记收集器类型、是否存在未知实验旗标。自动化不一定要很复杂,只要能挡住最常见的漏项就有价值。

10.5 写给仍然犹豫的人

如果你读完仍不确定选哪个,默认选 G1,并把精力转向分配剖析与存活集治理。不确定时选择可运维的默认值,不是保守怯懦,而是在信息不足时最小化后悔。等你拿到清晰的暂停证据与 CPU 预算,再来谈 ZGC,路径会更短,争议会更少。

反过来,如果你已经有充分证据却因为「我们一直用 G1」而拒绝评估,那也是另一种偏见。默认值是起点,不是宗教。架构判断力的标志是:知道何时坚持默认,也知道何时用证据打破默认。

至此,内存地图、分配路径、G1、ZGC、适合与不适合、生产纪律与决策矩阵已经串成一条完整链条。下一步请带着你服务的真实 GC 日志与 p99 曲线,把本文的决策矩阵填成一页纸的结论——那一页纸,才是这篇分享在团队里真正留下的东西。

10.6 参数与日志速查(JDK 17/21)

下面这些命令不构成调优配方,只是为了让讨论落到可执行层面。启用 G1 时关注 Mixed GC 是否跟上晋升;启用 ZGC 时关注并发阶段 CPU 与分配延迟。日志字段名称随版本可能变化,请以你的 JDK 文档为准。

常见观察维度包括:暂停时间的中位数与长尾、并发标记周期频率、堆占用在周期前后的回落幅度、老年代或等价存活集是否单调上升、以及应用层超时是否与暂停对齐。若暂停很短但超时仍高,问题多半不在收集器。若暂停很长且与超时严格对齐,才进入收集器或堆 sizing 讨论。

对于多副本服务,还要避免只看单副本。某个副本因流量不均或本地缓存更热,GC 行为可能显著差于平均值。对照实验应在多个副本上采样,或在路由层保证对比组流量等价。否则你会把流量不均误判成收集器差异。

10.7 结束前的自我追问

在提交变更前,请强制回答:我要优化的到底是哪一个百分位?我是否排除了锁、IO、下游与容器限流?我是否有可回滚版本?我是否愿意为结果支付 CPU 或内存成本?四个问题里有一个答不上来,就还不到切换的时候。把自我追问当作质量门禁,比任何单一参数更重要。

也请记住:优秀的 GC 选型文档读起来应该像一份约束说明书,而不像一份技术时尚评论。读者看完应能知道什么时候做、什么时候不做、做错了如何退回。若文档只能激发焦虑或焦虑性跟风,它就还没有达到本系列要求的架构判断力标准。

补强 · 阅读路径

11如果你只有三十分钟,应该带走什么

时间不够读完全文时,请至少掌握五句话。第一,先分清堆与非堆,再谈 GC。第二,分配速率与存活集决定压力,收集器只决定如何支付成本。第三,G1 是多数在线服务的理性默认。第四,ZGC 用 CPU 换尾延迟,必须用压测证明。第五,容器与泄漏问题优先于收集器切换。五句话背后的细节可以回看对应章节,但方向不能反。

若你是平台负责人,额外带走两句:统一观测与变更纪律,比统一收集器更重要;允许不同服务类型使用不同默认档,比强行单一标准更接近真实负载。若你是业务研发,额外带走两句:不要用对象池掩盖设计问题;不要在没有日志的情况下争论暂停。角色不同,行动清单不同,但证据标准相同。

也建议你在团队里做一次「反向演练」:假设有人强烈主张立刻全量上 ZGC,你要用本文的决策矩阵列出必须先满足的条件。演练的目的不是反对创新,而是反对无条件创新。能把反对说清楚的组织,通常也更擅长把创新做成。

阶段性结论

GC 知识的价值不在于记住更多缩写,而在于缩短「从现象到正确层级」的路径。选对层级,方法才便宜;选错层级,再好的收集器也像在错误地图上导航。