跳转至

垃圾收集器(Garbage Collectors)#

版本信息

JDK 8 开发者升到现代 JDK,默认垃圾收集器已从 Parallel 换成了 G1JEP 248,JDK 9 起),而当年常用的 CMS 已被彻底移除JEP 363,JDK 14)。本专题不讲「某版 GC 改了啥」(那在各版本的 JVM 与运行时 里讲),而是把全部主流 GC 横向摆在一起:各自什么原理、怎么配 JVM 参数、什么场景选它——以及升级时最该回答的「我那个 CMS / Parallel 怎么办」。

什么是垃圾收集,为什么 JDK 8 开发者该重新认识它#

垃圾收集(GC)自动回收 JVM 堆里「不再被引用的对象」,让开发者不必手动释放内存。但「怎么回收」直接决定三件事:停顿时长(STW)、吞吐量、内存占用。JDK 8 → 25 这十多年,GC 的核心矛盾从「尽量高吞吐」演变为「尽量低停顿、且停顿可预测」,并催生了 G1、ZGC、Shenandoah。

  • JDK 8 默认的 Parallel GC 追求吞吐,代价是 Full GC 整堆、长停顿。
  • 现代默认的 G1 把堆切成区域(region),优先回收垃圾最多的区域,停顿可控
  • ZGC / Shenandoah 把绝大部分回收工作与应用线程并发进行,做到亚毫秒停顿、且不随堆大小增长

升级 JDK,最大开箱即得的运行时红利往往就是 GC——多数应用什么都不配,停顿特性就已经比 JDK 8 好一大截。

先弄懂几个基础概念#

概念 一句话
堆分代 堆分为年轻代(Eden + 2 个 Survivor)与老年代。基于弱分代假说:绝大多数对象朝生夕死,所以频繁回收小而「垃圾多」的年轻代收益最高。
STW(Stop-The-World) GC 暂停所有应用线程。停顿就是指 STW 的时长;停顿越长,应用延迟越高。
Minor / Major / Full GC Minor = 回收年轻代(频繁、短);Major = 回收老年代;Full = 回收整个堆(最贵)。
并行(parallel)vs 并发(concurrent) 并行:多个 GC 线程同时干活,但应用线程仍暂停(STW)。并发:GC 线程与应用线程同时运行,只在极少数点短暂 STW。低延迟 GC(ZGC/Shenandoah/CMS)的关键就是把工作并发化。
GC 根(GC Roots) 可达性分析的起点:栈上的局部变量、静态字段、JNI 全局引用、活动线程等。「从根出发能到达的对象」是存活的,到不了的才是垃圾。
吞吐 vs 停顿 吞吐= 应用运行时间占总时间的比例(GC 占用越少越高);停顿= 单次 GC 暂停的时长。两者常冲突:追求吞吐可能允许较长单次停顿,追求低停顿可能牺牲一点吞吐。

演进主线#

graph LR
    S["Serial<br/>(JDK 1)"] --> P["Parallel<br/>(JDK 8 默认)"]
    CM["CMS<br/>(JDK 5)"] -->|JDK 14 移除| CMR["已移除"]
    CM --> G["G1<br/>(JDK 9 默认)"]
    P --> G
    G --> Z["ZGC<br/>(JDK 15 产品)"]
    G --> SH["Shenandoah<br/>(JDK 15 产品)"]
    Z --> ZG["分代 ZGC<br/>(JDK 21 / 25)"]
    SH --> SHG["分代 Shenandoah<br/>(JDK 25)"]
    style P fill:#ffe0b2
    style G fill:#c8e6c9
    style Z fill:#fff9c4
    style SH fill:#fff9c4
    style CMR fill:#ffcdd2

三条主线:吞吐时代(Serial→Parallel,全 STW)→ 可控停顿时代(G1 分区、停顿可预测;CMS 尝试并发但被淘汰)→ 低延迟时代(ZGC/Shenandoah 并发整理,亚毫秒停顿,并在 JDK 21/25 完成分代化以降开销)。

主流 GC 横向对比#

GC 引入 / 默认 状态 停顿特点 吞吐 适用场景 核心机制
Serial 自早期 JDK 在役 长(单线程 STW) 小堆(<~100MB)、单核、client 单线程复制 + 标记压缩
Parallel JDK 8 server 默认 在役 较长(多线程 STW) 批处理、离线 ETL、CPU 密集 并行复制 + 标记压缩
CMS JDK 5 引入 JDK 14 移除 较低(老年代并发) (历史)在线服务 并发标记-清除,不整理 → 碎片化
G1 JDK 7u4 / JDK 9 默认 在役(默认) 可控(分区 + 预测模型) 中高 大堆(>~4–6GB)、通用首选 Region + CSet + SATB 并发标记
ZGC JDK 11 实验 / JDK 15 产品 在役 亚毫秒,不随堆增长 大堆(GB~TB)、低延迟在线服务 染色指针 + 读屏障,并发转移
Shenandoah JDK 12 实验 / JDK 15 产品 在役(部分发行版) 低(亚毫秒级) 大堆、低延迟、Red Hat 系发行版 转发指针 + 读屏障,并发整理
Epsilon JDK 11(实验) 实验 · no-op 无(不回收,堆满即 OOM) 极高 性能基线、压测、短生命周期任务 只分配(bump-the-pointer)

详见各收集器专页:Serial 与 ParallelCMS(已移除)G1ZGCShenandoah

全维度对比:一张表看懂所有 GC#

把 7 个 GC 放在同一组维度上横向比(定性,便于选型时快速对照):

维度 Serial Parallel CMS G1 ZGC Shenandoah Epsilon
默认 / 状态 client 默认 JDK 8 server 默认 JDK 14 移除 JDK 9+ 默认 产品级 产品级(部分发行版) 实验
停顿 长(单线程 STW) 较长(多线程 STW) 较低(老年代并发) 可控 亚毫秒 亚毫秒 无(不回收)
吞吐 中高 极高
回收并发性 全 STW 全 STW 老年代并发 标记并发、转移 STW 几乎全并发 几乎全并发 不回收
碎片化 无(整理) 无(整理) 有(不整理) 无(整理)
分代 是(21 起,23 默认) 是(JDK 24+)
适用堆 < ~100MB 中(历史) 大(> ~4–6GB) 超大(GB~TB) 受控 / 短任务
调优复杂度 极简 简单 复杂 中等 简单(少调) 中等 极简
内存开销 较高(~15–30%) 中(复用 mark word) 极低

读法:停顿从左到右大体递减(Serial 最长 → ZGC 亚毫秒);吞吐以 Parallel 最高、Epsilon 极致;碎片化只有 CMS 是「有」——这正是它被移除的根因。选型时按「堆大小 + 延迟要求 + 吞吐诉求」三轴定位,详见下面的决策树。

该选哪个 GC?——选型决策树#

graph TD
    Q1{"堆大小 / 负载类型?"}
    Q1 -->|"小堆 / 单核"| SE["Serial<br/>-XX:+UseSerialGC"]
    Q1 -->|"批处理 / CPU 密集<br/>停顿不敏感"| PA["Parallel<br/>-XX:+UseParallelGC"]
    Q1 -->|"通用、堆较大<br/>停顿要可控"| G1N["G1(默认,无需显式指定)"]
    Q1 -->|"大堆 + 亚毫秒停顿<br/>在线服务"| Q2{"你的 JDK 发行版?"}
    Q2 -->|"多数 OpenJDK 构建"| ZG["ZGC<br/>-XX:+UseZGC"]
    Q2 -->|"Red Hat / Corretto 等"| SH["Shenandoah<br/>-XX:+UseShenandoahGC"]
    Q1 -->|"压测 / 不回收"| EP["Epsilon<br/>-XX:+UseEpsilonGC"]
    style G1N fill:#c8e6c9
    style ZG fill:#fff9c4
    style SH fill:#fff9c4

几条经验法则:

  • 升到 JDK 9+,默认就是 G1,多数应用什么都不用配。除非有明确的吞吐或延迟诉求,别轻易换。
  • 吞吐型负载(批处理、离线计算)才考虑 Parallel——它的 Full GC 停顿长,但稳态吞吐高。
  • 对延迟敏感、堆又大(几十 GB 以上、在线服务),ZGC 是现代首选;停顿几乎与堆大小无关。
  • Shenandoah 思路与 ZGC 类似,但只在特定发行版(Red Hat / Fedora / Amazon Corretto 等)可用——选之前先确认你的构建有没有。
  • Epsilon 不是用来生产的:它不回收,堆满即 OOM,只在隔离 GC 噪音做基线时用。

如何观测 GC#

判断「该用哪个 GC、配得对不对」靠观测,别靠猜:

# 打开 GC 日志(time/uptime/level/tags 等装饰)
java -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar
# JDK 8 的写法;JEP 271 后改为统一日志 -Xlog:gc*
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar app.jar

运行期探查(任意版本):

jcmd <pid> GC.heap_info      # 堆各代占用
jcmd <pid> GC.run            # 触发一次 GC
jcmd <pid> VM.flags          # 查看生效的 GC 相关 -XX 标志

在代码里识别当前 GC,看 GarbageCollectorMXBean 的名字(各 GC 的 bean 名见各专页)。生产常开 JFRJEP 328,JDK 11 起开源)做低开销持续诊断:

java -XX:StartFlightRecording=duration=60s,filename=app.jfr -jar app.jar

进一步阅读#

各收集器专页(原理 / JVM 参数 / 适用场景 / 常见坑):

各版本 JVM 更新(「这版 GC 改了啥」):

参考#