Java 垃圾回收器(Garbage Collector)详解
这篇文档讲的是 HotSpot JVM(Oracle/OpenJDK 系)里常见/主流的垃圾回收器。不同 GC 的目标不一样:有的追求吞吐,有的追求低延迟,有的追求“我只要不崩就行”。
1. 先把大框架捋清楚:GC 到底在管什么?
1.1 典型的堆分代(Generational Heap)
大多数 HotSpot GC 都默认走“分代”思路(ZGC / Shenandoah 也在逐步/已经支持分代能力):
- Young(新生代)
- Eden + S0/S1(Survivor)
- 对象大多“朝生夕死”,这里回收频繁
- Old(老年代)
- 存活时间长的对象
- 回收不频繁,但一次回收成本大
- (可选)Humongous/大对象区域
- G1 有 Humongous Region 的概念
1.2 GC 指标:你真正关心什么?
- 吞吐(Throughput):业务线程时间 / 总时间(越高越好)
- 延迟(Latency):STW(Stop-The-World)停顿时间(越低越好)
- 内存占用(Footprint):为了低延迟可能要更多“冗余空间”
- 可预测性(Predictability):停顿是否稳定、是否可控
你选 GC,其实是在做取舍:吞吐 vs 延迟 vs 内存。
2. HotSpot 常见垃圾回收器总览
按“类型”粗暴分组:
2.1 串行 / 并行(以吞吐为核心)
- Serial GC(串行):单线程 GC,简单粗暴
- Parallel GC(吞吐优先)
- Parallel Scavenge(新生代并行)
- Parallel Old(老年代并行)
2.2 并发低延迟(以减少停顿为核心)
- G1 GC:面向大堆的“平衡型”默认 GC(Java 9+ 默认)
- ZGC:超低延迟、可扩展到超大堆(并发压缩)
- Shenandoah:低延迟并发压缩(社区与部分发行版支持较强)
2.3 特殊用途
- Epsilon GC:不回收(用来压测/性能对比/让你 OOM 得更快更确定)
3. Serial GC(-XX:+UseSerialGC)
3.1 特点
- 单线程进行 Young/Old 回收
- STW 停顿相对明显,但实现简单、额外开销小
3.2 适用场景
- 单核/低核 CPU
- 小堆、简单服务、工具型程序
- 容器资源非常紧张,且延迟要求不高
3.3 常见参数
-XX:+UseSerialGC-Xms -Xmx:堆大小-Xmn或-XX:NewRatio:新生代比例
4. Parallel GC(吞吐优先)
4.1 Parallel Scavenge + Parallel Old
- 新生代:Parallel Scavenge
- 老年代:Parallel Old
- 典型目标:最大化吞吐(适合批处理、离线计算、吞吐型接口)
启用方式:
-XX:+UseParallelGC(通常会配套并行老年代)- 或显式:
-XX:+UseParallelOldGC
4.2 优点
- 多线程 GC,吞吐高
- 参数体系成熟、稳定
4.3 缺点
- STW 停顿可能比较长(尤其大堆/老年代回收)
- “延迟敏感”业务会难受
4.4 关键参数(吞吐调参常用)
-XX:ParallelGCThreads=<n>:并行 GC 线程数-XX:MaxGCPauseMillis=<ms>:目标停顿(不是保证)-XX:GCTimeRatio=<n>:GC 时间占比目标(吞吐优先时常用)-XX:+UseAdaptiveSizePolicy:自适应调节(默认一般开启)
经验:吞吐型服务可以先让 JVM 自适应跑起来,再用 GC 日志看瓶颈在哪(是频繁 Young GC,还是 Old GC 太重)。
5. CMS(Concurrent Mark Sweep)——历史地位很高,但已经退出舞台
CMS 曾经是低延迟的代表(“并发标记清除”),但它的主要问题是:
- 标记-清除导致碎片化(需要 Full GC 压缩救场)
- 维护成本高
在 JDK 14 中 CMS 已被移除(-XX:+UseConcMarkSweepGC 直接不可用)。
如果你线上还在用 CMS,要么你 JDK 很老(8/11),要么你应该尽快迁移到 G1 / ZGC / Shenandoah。
6. G1 GC(Garbage-First)
启用方式:
-XX:+UseG1GC
6.1 核心思想:Region + 预测停顿
G1 把整个堆切成很多 Region,不再严格固定“物理上的新生代/老年代连续空间”,而是用一组 Region 逻辑上组成 Young/Old。
它的目标:
- 在可控的停顿目标(
MaxGCPauseMillis)下, - 优先回收“垃圾最多(回收收益最大)”的 Region(Garbage-First)
6.2 G1 的回收阶段(简化版)
- Young GC(STW):复制/清理年轻代 Region
- 并发标记(Concurrent Marking):找出老年代活对象
- Mixed GC(STW):在 Young GC 基础上,顺手回收一部分老年代 Region
6.3 优点
- 相对可预测的停顿(不是绝对保证,但比纯并行/串行更可控)
- 大堆场景表现好(常见 4G~几十 G 都能扛)
- 默认 GC(Java 9+ 一般默认 G1)
6.4 缺点
- 调参空间大,配置不当容易“看不懂”
- 极端低延迟(比如 <10ms)和超大堆场景,ZGC/Shenandoah 往往更合适
6.5 关键参数(G1 常用)
-XX:MaxGCPauseMillis=<ms>:停顿目标(常用 50~200ms 取决于业务)-XX:InitiatingHeapOccupancyPercent=<percent>:触发并发标记阈值-XX:G1HeapRegionSize=<size>:Region 大小(一般让 JVM 自己选)-XX:ConcGCThreads=<n>:并发标记线程数-XX:ParallelGCThreads=<n>:STW 并行线程数
6.6 常见坑
- 频繁 Mixed GC:老年代增长太快,或晋升压力过大
- Humongous 对象问题:大对象直接占用多个 Region,容易造成回收收益低、堆碎片压力
- 优化方向:减少大对象分配(例如大数组、拼接),或调大 Region、或改对象生命周期
7. ZGC(Z Garbage Collector)
启用方式:
-XX:+UseZGC
7.1 它想解决什么?
目标是 超低停顿,并且停顿时间对堆大小不敏感(你把堆从 8G 拉到 128G,它也尽量不爆炸)。
ZGC 的关键点:
- 几乎所有重活都并发做,包括 并发整理/压缩(compaction)
- 依赖 读屏障(read barrier) 等机制来保证并发移动对象时引用一致
7.2 分代 ZGC(Generational ZGC)
- 在 JDK 21 引入“可选分代 ZGC”
- 在后续版本中,ZGC 的分代模式逐步成为默认方向(并计划移除非分代模式)
对你来说的直觉收益:
- 分代后 Young 对象回收更高效,整体吞吐和延迟一般会更好
7.3 优点
- 极低停顿(很适合对延迟抖动敏感的服务:交易/广告竞价/实时推荐/在线游戏等)
- 超大堆依然能保持较低暂停
7.4 缺点
- 对 CPU 更“吃”(并发工作多)
- 对内存空间有要求(低延迟往往意味着更需要“空间换时间”)
- 不是所有平台/发行版的支持程度完全一致(但主流 OpenJDK/Oracle JDK 支持很好)
7.5 常见参数
-XX:+UseZGC-Xms -Xmx:建议尽量相等,减少伸缩干扰-XX:ConcGCThreads=<n>:并发线程-XX:ZAllocationSpikeTolerance=<n>等(偏高级,建议先看日志再动)
8. Shenandoah GC(低停顿并发压缩)
启用方式(视发行版/版本而定):
-XX:+UseShenandoahGC
8.1 特色
- 和 ZGC 一样主打低停顿
- 也支持并发整理/压缩
- 同样依赖 barrier(屏障)来保证并发移动对象的正确性
8.2 优点
- 停顿很短、对大堆友好
- 在部分发行版(比如 Red Hat 系)生态很成熟
8.3 缺点
- 不同发行版/平台差异更明显一些(“能用”不代表“每家都一样快”)
- 调参资料和线上经验不如 G1 普及(但已经越来越多)
8.4 常见参数(示例)
-XX:+UseShenandoahGC-XX:ShenandoahGCHeuristics=<adaptive|static|compact|...>:启发式策略-XX:ShenandoahMinFreeThreshold=<percent>等
9. Epsilon GC(不回收)
启用方式:
-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
9.1 它有什么用?
- 做 性能基线:看看“没有 GC 干扰”的极限吞吐/延迟
- 压测内存泄漏:你会更快、更确定地 OOM
- 对比不同 GC 的开销
9.2 注意
线上别用(除非你明确知道你在干啥)。
10. 怎么选 GC?(实践导向)
10.1 先按业务目标选
- 吞吐优先(离线任务/批处理/日志分析)
- Parallel GC 往往性价比很高
- 综合平衡(大多数 Web 服务、业务系统)
- G1 通常是稳妥默认选择(尤其你不想折腾)
- 超低延迟(P99/P999 卡死就出事)
- ZGC / Shenandoah 优先尝试
- 配套要做:减少大对象、减少分配抖动、降低对象存活时间
10.2 你还得考虑:JDK 版本
- 老项目还在 JDK 8:你可能还在 Parallel/CMS 时代(建议评估升级)
- JDK 11/17/21:G1/ZGC/Shenandoah 选择空间更大,特性更成熟
11. 必会:GC 日志怎么看?
11.1 建议统一打开 GC 日志(JDK 9+)
-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags
JDK 8(老格式):
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
11.2 重点看哪些指标?
- Young GC 频率、每次停顿
- Old/Mixed/Full GC 发生原因与频率
- 晋升失败(promotion failed)、to-space exhausted
- 并发周期是否跟得上分配速率(并发标记来不及会触发更重的停顿)
12. 常见问题与定位思路(非常实用)
12.1 “GC 很频繁”通常意味着什么?
- 堆太小 / 新生代太小
- 对象分配太快(QPS、缓存、日志、拼接)
- 大对象太多(byte[]、char[]、大 List/Map)
处理顺序建议:
- 先确认是不是内存泄漏(Old 一直涨、回收后不降)
- 再看是不是分配速率过高(Young 过密)
- 再考虑扩堆 / 调整新生代比例 / 优化对象创建
12.2 “Full GC 停顿很长”
- G1:Mixed 来不及回收老年代,退化到 Full GC
- Parallel:老年代回收天然 STW,很正常但可能超出 SLA
- CMS(如果你还在用老 JDK):碎片化触发压缩
解决方向:
- 减少老年代压力:缓存策略、对象生命周期、减少晋升
- 增大堆 + 优化分配
- 迁移更低延迟 GC(ZGC/Shenandoah)
12.3 “CPU 飙高但 GC 日志看起来还行”
- 可能是并发 GC 在吃 CPU(ZGC/Shenandoah/G1 并发阶段)
- 也可能是业务线程本身问题(锁竞争/IO/自旋)
做法:
- 上
async-profiler/JFR看火焰图 - 对照 GC 日志时间线,看 CPU 峰值是否和 GC 并发周期重合
13. 一句话总结(帮你记牢)
- Serial:小堆/简单场景,能用但停顿明显
- Parallel:吞吐怪兽,延迟不友好
- G1:默认稳妥的“平衡型”选择,适配面广
- ZGC:低延迟王者之一,适合大堆+低停顿
- Shenandoah:低延迟另一条路线,部分发行版更强
- Epsilon:不回收,用来对比/压测/找泄漏
14. 附:常用启用示例
14.1 G1(通用 Web 服务)
java -Xms4g -Xmx4g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags \
-jar app.jar
14.2 ZGC(低延迟)
java -Xms8g -Xmx8g \
-XX:+UseZGC \
-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags \
-jar app.jar
14.3 Parallel(吞吐优先)
java -Xms4g -Xmx4g \
-XX:+UseParallelGC \
-Xlog:gc*:file=gc.log:time,uptime,level,tags \
-jar batch.jar
15. 推荐学习路线(不绕弯子)
- 先把 GC 日志看懂(你调参的“仪表盘”)
- 熟悉 G1(最常见)
- 再学 ZGC / Shenandoah(低延迟场景)
- 最后学“调优方法论”:先定位问题,再动参数,别上来就堆参数
241

被折叠的 条评论
为什么被折叠?



