4. 垃圾收集器原理与选型

垃圾收集器是 JVM 内存管理中真正决定停顿表现的部分。堆结构、对象分配、可达性分析只是“准备条件”,而收集器决定的是:什么时候回收、用多少线程回收、是否移动对象、停顿能压到多低。选错收集器,往往不是吞吐量差一点,而是延迟指标直接失控。因此理解各收集器的机制差异,比记住“G1 适合大堆、ZGC 适合低延迟”这类结论更重要。

一、先看分代假设与收集器演进

绝大多数收集器都建立在弱分代假设上:大部分对象朝生夕死,少数对象长期存活。于是堆被划分为年轻代和老年代,年轻代又分为 Eden 与两个 Survivor 区。新对象优先在 Eden 分配,Minor GC 时把存活对象复制到 Survivor,经历多次回收仍存活的对象晋升到老年代。当老年代空间不足时触发 Full GC 或混合回收。

不同收集器的差异,本质上是在“吞吐量、停顿时间、内存占用”三者之间做不同取舍。吞吐量关注单位时间内完成的有效工作比例;停顿时间关注单次 GC 让应用暂停多久;内存占用关注收集器自身需要的额外结构。三者不可兼得,选型就是根据业务 SLA 决定优先保谁。

六种垃圾收集器机制与适用场景对照 收集器 核心机制 停顿特征 适用堆规模 典型适用场景 Serial 单线程复制/标记整理 单线程串行回收 全程 STW 停顿较长 与堆大小正相关 几十 MB ~ 百 MB 小堆 客户端程序、嵌入式、单核容器、 对停顿不敏感的批处理 Parallel 多线程复制/标记整理 多线程并行回收 全程 STW 停顿中等 可调 MaxGCPauseMillis 百 MB ~ 数 GB 中等堆 吞吐优先的后台计算、离线任务、 大数据批处理 CMS 并发标记清除 初始/重新标记 STW 并发标记与清除 停顿短 但易碎片化 数 GB 中等偏大堆 JDK 8 时代低延迟 Web 应用, 现已不推荐新项目使用 G1 Region 分区 + 复制 可预测停顿模型 按回收价值优先 停顿可控 目标 MaxGCPauseMillis 数 GB ~ 数十 GB 大堆 JDK 9+ 默认,服务端通用首选, 兼顾吞吐与延迟 ZGC 染色指针 + 读屏障 并发标记/转移/重定位 几乎全并发 亚毫秒级停顿 <1ms(JDK 16+) 数十 GB ~ TB 超大堆 超低延迟服务、大内存实时系统、 金融交易、游戏服务器 Shenandoah Brooks 转发指针 并发压缩整理 低停顿 停顿与堆大小无关 10ms 级目标 数 GB ~ 数百 GB 中大堆 OpenJDK 低延迟场景, 对停顿敏感但堆未达 TB 级 吞吐优先 平衡 延迟优先

二、Serial 与 Parallel:吞吐量优先的两种形态

Serial 是最简单的收集器,年轻代用复制算法,老年代用标记整理算法,全程单线程 STW。它的优势是额外内存开销小、没有线程交互成本,在单核或小堆环境下反而效率最高。启动参数为 -XX:+UseSerialGC。如果应用堆只有几百 MB、部署在单核容器中,Serial 的停顿可能只有几十毫秒,完全可接受。

Parallel 又称吞吐量收集器,把 Serial 的单线程换成多线程并行回收,年轻代用 Parallel Scavenge,老年代用 Parallel Old。它不追求最短停顿,而是追求最高吞吐量。关键参数是 -XX:MaxGCPauseMillis 和 -XX:GCTimeRatio,前者设置期望最大停顿,后者设置 GC 时间占比。例如 -XX:GCTimeRatio=99 表示希望 GC 时间不超过总时间的 1%。Parallel 适合离线计算、报表生成、大数据批处理这类“跑完就行、中间卡一下无所谓”的任务。

三、CMS:并发标记清除的得与失

CMS 是第一款真正意义上的并发收集器,目标是把停顿压到最低。它把回收拆成四个阶段:初始标记、并发标记、重新标记、并发清除。初始标记和重新标记需要 STW,但时间很短;并发标记和并发清除与应用线程同时运行。启用参数为 -XX:+UseConcMarkSweepGC。

CMS 的问题在于标记清除算法不移动对象,长期运行会产生大量内存碎片。当老年代没有连续空间分配大对象时,会触发 Concurrent Mode Failure,退化为 Serial Old 做一次 Full GC,停顿反而更长。此外 CMS 还有浮动垃圾问题,并发清除阶段新产生的垃圾只能留到下次回收。因此 CMS 在 JDK 9 被标记废弃,JDK 14 正式移除。新项目不应再选 CMS,但维护老系统时需要理解它的退化路径。

四、G1:面向大堆的可预测停顿收集器

G1 把堆划分为大小相等的 Region,每个 Region 可以扮演 Eden、Survivor 或 Old 角色。它不再要求物理连续的分代,而是用逻辑分代管理。G1 的核心是停顿预测模型:维护每个 Region 的回收价值(回收能释放多少空间、需要多少时间),每次 Young GC 或 Mixed GC 时优先回收价值最高的 Region,从而在指定停顿目标内获得最大收益。

G1 的回收过程包括初始标记、并发标记、最终标记、筛选回收。其中并发标记与筛选回收阶段部分并发,停顿可控。关键参数:

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200        # 期望最大停顿,默认 200ms
-XX:G1HeapRegionSize=16m        # Region 大小,1~32MB,2 的幂
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记的堆占用阈值
-XX:G1NewSizePercent=5          # 年轻代最小占比
-XX:G1MaxNewSizePercent=60      # 年轻代最大占比

G1 适合堆在 4GB 到数十 GB、要求停顿在 200ms 以内的服务端应用。JDK 9 起 G1 成为默认收集器。调优时不要同时设置 -Xmn 和 MaxGCPauseMillis,否则会干扰 G1 自适应调整年轻代大小。

五、ZGC 与 Shenandoah:亚毫秒停顿的两种实现

ZGC 的目标是任意堆大小下停顿都不超过 1ms。它依靠染色指针和读屏障实现并发标记、并发转移、并发重定位。染色指针把标记信息存在指针本身而不是对象头,读屏障在应用线程读取引用时修正指针,从而允许对象在应用运行期间被移动。ZGC 在 JDK 11 引入,JDK 15 转正,JDK 16 起停顿进入亚毫秒级。启用参数为 -XX:+UseZGC,JDK 21 起支持分代 ZGC:-XX:+UseZGC -XX:+ZGenerational。

Shenandoah 由 Red Hat 主导,思路与 ZGC 类似但实现不同:它使用 Brooks 转发指针,每个对象额外维护一个转发字段,并发压缩整理时通过转发指针定位新地址。Shenandoah 的停顿同样与堆大小无关,目标在 10ms 级。启用参数为 -XX:+UseShenandoahGC。两者都适合超低延迟场景,区别在于 ZGC 对超大堆(TB 级)更友好,Shenandoah 在中等堆上内存开销更可控。

六、选型决策与实操建议

选型时先明确延迟目标,再看堆规模,最后看 JDK 版本。下面给出一个可直接对照的决策表:

场景推荐收集器关键参数判断依据
单核小堆客户端Serial-XX:+UseSerialGC堆 < 500MB,停顿可接受
离线批处理、吞吐优先Parallel-XX:+UseParallelGC -XX:GCTimeRatio=99不关心单次停顿
JDK 8 老系统低延迟CMS(维护)-XX:+UseConcMarkSweepGC仅维护,不新选
通用服务端、堆 4~32GBG1-XX:+UseG1GC -XX:MaxGCPauseMillis=200JDK 9+ 默认
超低延迟、堆 > 32GBZGC-XX:+UseZGC停顿 < 1ms 要求
低延迟、中等堆Shenandoah-XX:+UseShenandoahGC停顿与堆大小无关
实操建议:先用 -Xlog:gc*:file=gc.log:time,uptime,level,tags 打开 GC 日志,观察 GC 频率、停顿分布和堆增长趋势,再决定是否换收集器。不要在没有日志依据的情况下盲目切换。
注意:ZGC 和 Shenandoah 会带来额外的内存开销和 CPU 占用。ZGC 的染色指针需要预留地址空间,Shenandoah 的 Brooks 指针每个对象多一个字段。如果堆本身很小,用它们反而得不偿失。

最后强调一个常见误区:收集器不是越新越好。G1 在 8GB 堆、200ms 停顿目标下表现稳定;ZGC 在 4GB 堆上未必比 G1 吞吐更高。选型的本质是让收集器的取舍方向与业务的 SLA 方向一致。延迟敏感选 ZGC 或 Shenandoah,吞吐敏感选 Parallel,通用场景选 G1,资源受限选 Serial。理解机制,才能在看 GC 日志时判断“这次停顿是正常的还是退化了”。