# JVM实战:垃圾收集器及其适用场景

# 垃圾收集器
图中展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。在JDK8时将Serial+CMS,ParNew+Serial Old这两个组合声明为废弃,并在JDK9中完全取消了这些组合的支持
并行和并发都是并发编程中的专业名词,在谈论垃圾收集器的上下文语境中, 它们可以理解为
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
并发(Concurrent):指用户线程与垃圾收集线程同时执行
# 传统分代收集器(基于物理分代)
# Serial / Serial Old 收集器
特点:单线程工作。在进行垃圾收集时,必须暂停其他所有的工作线程(即著名的 Stop The World),直到它收集结束
算法:新生代采用标记-复制算法。老年代采用标记-整理算法
场景:适合内存资源受限(如几百MB)或单核 CPU 的客户端(Client)模式应用,因为没有线程切换开销,单线程效率极高

# ParNew收集器
特点:Serial 收集器的多线程并行版本。在 GC 时同样需要 STW,但会利用多个 CPU 核心并行进行垃圾回收
场景:主要为了配合老年代的 CMS 收集器 使用,曾是服务端(Server)模式下非常核心的新生代收集器

# Parallel Scavenge / Parallel Old 收集器
特点:多线程。但它的核心关注点是吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+运行垃圾收集时间)
算法:新生代采用标记复制算法,老年代采用标记-整理算法
场景:适合后台运算、对响应时间要求不高但对吞吐量敏感的场景,如大批量数据处理、科学计算等任务

# CMS收集器
特点:老年代收集器,以获取最短回收停顿时间(低延迟)为目标。它是 JVM 历史上第一款真正意义上的并发(Concurrent)收集器,允许垃圾收集线程和用户线程同时工作
算法:标记-清除算法
核心步骤:初始标记(STW)-> 并发标记 -> 重新标记(STW)-> 并发清除
缺点:由于采用标记-清除算法,会产生大量的内存碎片;且对 CPU 资源非常敏感。在 JDK 9 中被标记为废弃,JDK 14 中被正式移除

- 初始标记:标记一下GC Roots能直接关联到的对象,速度很快(这一步会发生STW)
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集一起并发运行
- 重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(就是三色标记法中的增量更新,这一步也会发生STW)
- 并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以看这个阶段也是可以与用户线程同时并发的
因为目前ParNew+CMS的组合最常用
# 现代分区收集器(基于逻辑分代/无分代)
# G1收集器
特点:JDK 9 开始的默认垃圾收集器,将整个堆内存划分成多个大小相等的独立区域(Region)。虽然它依然保留了 Eden、Survivor 和 Old 的概念,但它们不再物理隔离,而是动态映射的。一个Region有可能属于新生代有可能属于老年代,新生代和老年代的内存区域是不停变动的,由G1自动控制 
核心机制:在G1之前的垃圾收集器,当发生GC的时候,要么回收整个新生代,要么回收整个老年代。而G1可以面向堆内存的任何部分进行回收,Region是单次回收的基本单元。G1 会跟踪各个 Region 里面垃圾的“价值”(即回收能释放的空间和所需时间的经验值),在后台维护一个优先级列表,每次根据用户指定的 预测停顿时间(如:希望停顿不超过 200ms),优先回收价值最大的 Region
算法: G1从整体来看是基于标记-整理算法实现的收集器, 但从局部(两个Region之间) 上看又是基于标记-复制算法实现,因此不会产生内存碎片
G1收集器的运行过程如下
- 初始标记:标记一下GC Roots能直接关联到的对象,速度很快(这一步会发生STW)
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集一起并发运行
- 最终标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(就是三色标记法中的原始快照,这一步也会发生STW)
- 筛选回收:根据用户期望的停顿时间,回收部分Region。首先把要回收的Region中的存活对象复制到空的Region,在清理掉旧Region的空间
# ZGC收集器
特点:在 JDK 11 中引入,JDK 15 转正,是一款革命性的低延迟垃圾收集器
核心优势:无论你的堆内存是几百 MB 还是几个 TB,ZGC 都能将 STW 停顿时间控制在 10 毫秒以内(甚至 1 毫秒以内)。它的绝大部分标记和转移阶段都是与用户线程并发执行的
关键技术:使用了染色指针(Colored Pointers)和读屏障(Load Barriers)技术
场景:超大堆、超低延迟的密集型生产环境(如金融、高频交易、高并发即时响应系统)
# 总结
表格中复制指代标记-复制算法,整理指代标记-整理算法
| 垃圾收集器 | 作用区域 | 算法 | 核心目标 | 适用场景 |
|---|---|---|---|---|
| Serial / Serial Old | 新生代/老年代 | 复制/整理 | 内存占用小、简单高效 | 单核 CPU、微型客户端应用 |
| Parallel Scavenge /Parallel Old | 新生代/老年代 | 复制/整理 | 高吞吐量 | 后台批处理、科学计算 |
| ParNew | 新生代 | 复制 | 减少新生代停顿时间(并行) | 常与老年代 CMS 组合使用,曾是主流服务端配置 |
| CMS | 老年代 | 标记-清除 | 低延迟 | 互联网服务端(对响应时间有要求),因碎片问题已被 G1/ZGC 替代 |
| G1 | 整个堆(分区) | 复制 + 整理 | 可预测的低延迟 | 中大型堆(>4G)的服务端应用(JDK 9 开始的默认组合) |
| ZGC | 整个堆(分区) | 复制(并发) | 极致低延迟(<10ms) | 超大堆、高并发、对卡顿极度敏感的系统 |
# 附录
# CMS有哪些问题?
1.并发回收垃圾导致CPU资源紧张
并发标记和并发清理阶段,垃圾回收线程和系统工作线程同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分
2.无法处理浮动垃圾
在并发清理阶段,CMS回收的是之前标记好的对象,但是这个阶段系统一直在运行,有可能会产生新的垃圾对象,这种垃圾对象就是“浮动垃圾”,浮动垃圾只能等到下一次GC才会被回收
3.Concurrent Mode Failure导致垃圾收集器切换到SerialOld
如果在CMS垃圾回收期间,程序要放入老年代的对象大于了可用内存空间,会发生Concurrent Mode Failure,就是说并发垃圾回收失败,我一边回收,你一边把对象放入老年代,内存都不够用了。此时就会用SerialOld替换CMS,直接 Stop the World 进行垃圾回收,一次性把垃圾对象都回收掉
4.CMS用的是标记清除算法,会导致大量的内存碎片