垃圾收集器(Garbage Collectors)#
版本信息
- 范围:跨版本专题,串起 JDK 8 → 25 的 GC 演进,横向对比全部主流收集器
- 本专题覆盖:Serial | Parallel | CMS(已移除)| G1(默认) | ZGC(低延迟) | Shenandoah | Epsilon
- 关键 JEP:248 G1 默认(9)|291 弃用 CMS(9)|363 移除 CMS(14)|377 ZGC 产品级(15)|379 Shenandoah 产品级(15)|439 分代 ZGC(21)|521 分代 Shenandoah(25)
JDK 8 开发者升到现代 JDK,默认垃圾收集器已从 Parallel 换成了 G1(JEP 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 与 Parallel | CMS(已移除) | G1 | ZGC | Shenandoah。
全维度对比:一张表看懂所有 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 名见各专页)。生产常开 JFR(JEP 328,JDK 11 起开源)做低开销持续诊断:
java -XX:StartFlightRecording=duration=60s,filename=app.jfr -jar app.jar
进一步阅读#
各收集器专页(原理 / JVM 参数 / 适用场景 / 常见坑):
各版本 JVM 更新(「这版 GC 改了啥」):
- JDK 11 JVM 与运行时:G1 默认化、Epsilon、ZGC 实验
- JDK 17 JVM 与运行时:ZGC / Shenandoah 转正
- JDK 21 JVM 与运行时:分代 ZGC
- JDK 25 JVM 与运行时:ZGC 分代收尾、Generational Shenandoah