一、什么是垃圾收集器?
上一篇我们学习了三种经典垃圾回收算法:
- 标记-清除算法
- 标记-复制算法
- 标记-整理算法
这些算法解决的是:如何回收垃圾对象?
而垃圾收集器(Garbage Collector)则是这些算法在 JVM 中的具体实现。
可以这样理解:
- 垃圾回收算法:提供回收思路。
- 垃圾收集器:将算法、内存组织方式、线程调度和具体回收流程组合起来,真正执行垃圾回收。
不同垃圾收集器的设计目标并不相同。有的追求吞吐量,有的追求低停顿时间,有的则希望在两者之间取得平衡。
JDK 8 中经常接触到的垃圾收集器包括:
- Serial
- ParNew
- Parallel Scavenge
- Serial Old
- Parallel Old
- CMS
- G1

二、并行与并发
在学习具体收集器之前,需要先区分两个非常容易混淆的概念。
1. 并行收集(Parallel GC)
并行强调的是:多个垃圾收集线程同时工作。
例如,原本只有一个 GC 线程负责回收,现在可以启动多个 GC 线程,分别处理不同区域或对象。
但要注意:
并行垃圾回收期间,应用线程通常仍然需要暂停,也就是发生 Stop The World(STW)。
所以:
并行不代表应用程序可以继续运行。
2. 并发收集(Concurrent GC)
并发强调的是:垃圾收集线程与应用程序线程在一段时间内同时运行。
例如:
- GC 线程正在遍历对象、标记存活对象。
- 应用线程仍然可以继续执行代码。
这样可以减少应用程序完全暂停的时间。
但是,并发也不意味着完全没有 STW。很多并发收集器仍然需要在某些关键阶段暂停应用线程。
例如 CMS 就有初始标记和重新标记两个主要 STW 阶段。

3. 并行和并发的区别
| 对比 | 并行(Parallel) | 并发(Concurrent) |
|---|---|---|
| 核心含义 | 多个 GC 线程同时工作 | GC 线程与应用线程同时运行 |
| 应用线程 | 通常暂停 | 部分阶段可以继续运行 |
| 主要目标 | 缩短 GC 工作本身的耗时 | 减少应用程序停顿时间 |
| 典型代表 | Parallel Scavenge、Parallel Old | CMS、G1 的部分阶段 |
记住:
- 并行:多个 GC 线程一起干活。
- 并发:GC 和应用程序可以同时干活。
三、Serial 收集器
Serial 是比较基础的一种垃圾收集器。
它主要有两个特点:
- 使用单个 GC 线程进行垃圾回收。
- 垃圾回收期间,应用线程需要暂停。
Serial 收集器主要用于新生代,采用标记-复制的回收思路。
当发生 Young GC 时:
text
应用线程暂停
|
v
单个 GC 线程执行回收
|
v
回收完成
|
v
应用线程恢复
1. Serial 的优点
- 实现简单。
- 没有多线程竞争和协作的额外开销。
- 在堆较小、处理器资源有限的场景下,表现可能不错。
2. Serial 的缺点
它使用单线程进行垃圾回收。当堆内存较大、需要处理的对象较多时,单个 GC 线程可能需要较长时间才能完成回收。
3. 如何开启 Serial?
bash
-XX:+UseSerialGC
在常见 HotSpot 配置中,使用该参数会选择 Serial 新生代收集器及其配套的 Serial Old 老年代收集器。
四、ParNew 收集器
ParNew 可以理解为 Serial 的多线程版本。
它同样主要用于新生代,也采用标记-复制的回收思路。
与 Serial 最大的区别是:
ParNew 使用多个 GC 线程并行回收。
例如:
text
GC 线程 1:处理一部分对象
GC 线程 2:处理另一部分对象
GC 线程 3:处理另一部分对象
多个线程共同完成新生代的垃圾回收。
不过,ParNew 回收时仍然会暂停应用线程。
1. ParNew 的特点
- 新生代收集器。
- 多个 GC 线程并行执行。
- 采用复制算法的思路。
- 适合与 CMS 老年代收集器配合使用。
2. 为什么 ParNew 经常和 CMS 一起出现?
因为 CMS 是老年代收集器,而 ParNew 是新生代收集器。
在经典 HotSpot 分代收集器组合中,ParNew + CMS 是一个非常典型的搭配。
ParNew 负责新生代,CMS 负责老年代。
3. 如何开启 ParNew?
bash
-XX:+UseParNewGC
需要注意:ParNew 是比较典型的历史分代收集器。在较新的 JDK 版本中,其使用场景和可用性已经发生变化。学习 JDK 8 时重点掌握它与 CMS 的经典组合即可。
五、Parallel Scavenge 收集器
Parallel Scavenge 也是一个新生代收集器,同样采用并行的复制回收思路。
它和 ParNew 看起来很相似,但设计目标有所不同。
Parallel Scavenge 更加关注吞吐量。
1. 什么是吞吐量?
吞吐量可以简单理解为:
text
吞吐量 = 用户代码运行时间 /(用户代码运行时间 + GC 时间)
例如:
假设一个程序运行了 100 秒:
- 用户代码执行了 90 秒。
- GC 总共执行了 10 秒。
那么:
text
吞吐量 = 90 / 100 = 90%
吞吐量越高,说明单位时间内用于执行用户代码的时间占比越高。
2. Parallel Scavenge 的特点
- 新生代收集器。
- 多线程并行回收。
- 采用复制算法的思路。
- 以吞吐量为主要优化目标。
- 支持自适应调节策略。
3. 什么是自适应调节策略?
Parallel Scavenge 提供了一个非常有代表性的参数:
bash
-XX:+UseAdaptiveSizePolicy
开启后,JVM 可以根据运行时情况动态调整:
- Eden 区大小。
- Survivor 区大小。
- 新生代与老年代的比例等相关参数。
目标是尽量达到用户设置的性能目标,而不是始终使用固定的内存比例。
例如:
如果用户更关注吞吐量,JVM 就可能根据运行情况调整 GC 和内存参数,让程序尽可能多地执行用户代码。
4. 如何设置吞吐量目标?
例如:
bash
-XX:MaxGCPauseMillis=200
-XX:GCTimeRatio=99
其中:
MaxGCPauseMillis:期望的最大 GC 停顿时间目标,不是严格保证。GCTimeRatio:用于设置 GC 时间与用户代码执行时间之间的目标比例。
注意:Parallel Scavenge 的目标是尽量满足性能目标,而不是承诺每次 GC 都不会超过指定停顿时间。
六、Serial Old 和 Parallel Old
前面介绍的 Serial、ParNew、Parallel Scavenge 都是新生代收集器。
那么老年代由谁负责回收?
这就要介绍 Serial Old 和 Parallel Old。
1. Serial Old
Serial Old 是 Serial 收集器的老年代版本。
它主要特点:
- 老年代收集器。
- 使用单个 GC 线程。
- 采用标记-整理算法。
- 回收时应用线程暂停。
它的一个重要用途是作为 CMS 在某些情况下的后备收集器。
2. Parallel Old
Parallel Old 是 Parallel Scavenge 的老年代版本。
它主要特点:
- 老年代收集器。
- 多个 GC 线程并行执行。
- 采用标记-整理算法。
- 以吞吐量为主要优化目标。
3. 两者有什么区别?
| 对比 | Serial Old | Parallel Old |
|---|---|---|
| 所属区域 | 老年代 | 老年代 |
| GC 线程 | 单线程 | 多线程 |
| 算法 | 标记-整理 | 标记-整理 |
| 主要特点 | 实现简单 | 并行回收,关注吞吐量 |
七、CMS 收集器
CMS(Concurrent Mark Sweep)是 JDK 8 中非常重要的一个垃圾收集器。
它的设计目标是:
尽可能缩短老年代垃圾回收时应用线程的停顿时间。
CMS 主要采用标记-清除算法,并通过让部分 GC 阶段与应用线程并发执行,降低 STW 时间。
1. CMS 的整体工作流程
CMS 的主要流程包括四个阶段:
- 初始标记(Initial Mark)
- 并发标记(Concurrent Mark)
- 重新标记(Remark)
- 并发清除(Concurrent Sweep)

2. 初始标记(Initial Mark)
初始标记阶段需要暂停所有应用线程,也就是发生 STW。
它主要标记 GC Roots 直接关联的对象。
这个阶段通常比较快,因为它不需要完成整个对象图的遍历。
3. 并发标记(Concurrent Mark)
并发标记阶段,GC 线程和应用线程可以同时执行。
GC 线程从初始标记的对象出发,继续遍历对象引用关系,标记其他存活对象。
由于应用线程仍然在运行,对象引用关系可能发生变化,因此这一阶段标记的结果不一定是最终结果。
4. 重新标记(Remark)
重新标记阶段需要再次暂停应用线程。
它主要用于修正并发标记阶段中,由于应用线程继续运行、引用关系发生变化而产生的标记遗漏。
这个阶段是 CMS 中另一个比较重要的 STW 阶段。
可以这样理解:
- 初始标记:先标记 GC Roots 直接关联对象。
- 并发标记:继续遍历对象图。
- 重新标记:修正并发期间引用变化带来的标记结果。
- 并发清除:回收未存活对象。
5. 并发清除(Concurrent Sweep)
这个阶段 GC 线程会与应用线程并发执行。
垃圾收集器清除未存活对象占用的空间,存活对象通常不需要整体移动。
因此 CMS 主要采用的是标记-清除思路。
6. CMS 有什么缺点?
CMS 虽然能够降低停顿时间,但也存在一些问题。
(1)内存碎片
CMS 采用标记-清除算法,不会在正常回收流程中整体整理存活对象,因此可能产生内存碎片。
(2)浮动垃圾(Floating Garbage)
在并发清除阶段,应用线程仍然运行,可能产生新的垃圾对象。
这些对象可能无法被本轮 CMS 回收,只能留到下一次 GC 再处理。
这部分垃圾通常称为浮动垃圾。
(3)对 CPU 资源敏感
CMS 在并发阶段需要占用 CPU 资源。
如果 CPU 核数较少,GC 线程和应用线程之间可能产生资源竞争,影响应用性能。
(4)并发模式失败(Concurrent Mode Failure)
如果老年代空间不足,CMS 还没有完成回收,应用线程又持续分配对象,就可能导致老年代无法满足分配需求。
此时 CMS 可能无法继续按预期完成并发回收,需要触发更重的 Full GC,并可能使用 Serial Old 等后备方式进行回收。
7. 如何开启 CMS?
bash
-XX:+UseConcMarkSweepGC
在经典 JDK 8 HotSpot 配置中,CMS 通常与 ParNew 配合使用。
需要注意版本变化:CMS 从 JDK 9 开始被标记为废弃,JDK 14 中被移除。现在学习 CMS,主要是为了理解经典并发标记-清除收集器的工作原理,以及历史项目中的 GC 日志。
八、G1 收集器
G1(Garbage First)是一个非常重要的垃圾收集器。
与前面传统的分代收集器不同,G1 不再使用固定连续的内存区域来组织整个新生代和老年代,而是将堆划分为多个大小相等的 Region。
每个 Region 在不同时间可以承担不同角色,例如:
- Eden Region。
- Survivor Region。
- Old Region。
- Humongous Region。
1. G1 的核心思想
G1 的核心思想是:
将整个堆划分成多个 Region,根据回收收益选择一部分 Region 进行回收。
例如,某些 Region 中垃圾对象比较多,存活对象比较少。
那么回收这些 Region,可能只需要复制少量存活对象,就能释放较多内存。
因此 G1 可以优先选择回收收益比较高的 Region。

2. G1 的回收方式
G1 常见的回收类型包括:
Young GC
主要回收年轻代 Region。
G1 会将存活对象疏散到其他 Survivor Region 或老年代 Region,回收过程会产生 STW。
Mixed GC
在并发标记之后,G1 除了回收年轻代 Region,还会选择部分收益较高的老年代 Region 一起回收。
因此 Mixed GC 不等于 Full GC。
3. G1 为什么能够控制停顿时间?
G1 可以根据用户设置的停顿时间目标,结合 Region 的回收收益和预计耗时,选择本轮要回收的区域。
例如:
bash
-XX:MaxGCPauseMillis=200
它表示期望的最大停顿时间目标。
G1 会尽量在目标范围内选择回收工作量,但这不是严格的时间保证。
4. G1 和 CMS 有什么区别?
| 对比 | CMS | G1 |
|---|---|---|
| 内存组织 | 传统分代区域 | Region |
| 回收算法思路 | 标记-清除 | 标记、疏散复制等 |
| 内存碎片 | 可能产生 | 通过疏散回收改善碎片问题 |
| 回收范围 | 主要针对老年代 | 可以管理整个堆 |
| 停顿目标 | 低停顿 | 可设置停顿时间目标 |
| 典型特点 | 并发标记清除 | Region 化、收益优先回收 |
需要注意:G1 并不是简单地把 CMS 换成了复制算法。它内部还有并发标记、Remembered Set、SATB 等机制,后续可以单独深入学习。
九、常见垃圾收集器如何选择?
不同垃圾收集器的优化目标不同,选择时需要结合应用场景。
| 场景 | 可以考虑的收集器 | 原因 |
|---|---|---|
| 堆较小、资源有限 | Serial | 单线程收集,管理开销相对简单 |
| 希望提高吞吐量 | Parallel Scavenge + Parallel Old | 以吞吐量为主要目标 |
| 经典低停顿需求(JDK 8) | ParNew + CMS | 部分阶段与应用线程并发 |
| 希望控制停顿时间、管理较大堆 | G1 | Region 化管理,按收益选择回收区域 |
不要简单地认为某个收集器一定比另一个好。
例如:
- CMS 的并发阶段会占用 CPU。
- Parallel 收集器可能获得更高吞吐量,但应用线程需要经历 STW。
- G1 的停顿目标也不是严格保证,具体表现仍然取决于堆大小、对象分配速率和业务负载。
十、本篇总结
- 垃圾回收算法提供回收思路,垃圾收集器是 JVM 中具体执行垃圾回收的实现。
- 并行强调多个 GC 线程同时工作,并发强调 GC 线程与应用线程同时运行。
- Serial 使用单个 GC 线程进行回收,简单但可能停顿较长。
- ParNew 是新生代并行收集器,经典场景下经常与 CMS 配合。
- Parallel Scavenge 关注吞吐量,支持自适应调节策略。
- Serial Old 使用单线程标记-整理算法;Parallel Old 使用多线程标记-整理算法。
- CMS 主要采用标记-清除思路,包含初始标记、并发标记、重新标记和并发清除四个阶段。
- CMS 可能产生浮动垃圾和内存碎片,并且存在并发模式失败的风险。
- G1 将堆划分为多个 Region,能够按回收收益选择 Region 进行疏散回收。
- G1 的 Young GC 回收年轻代 Region,Mixed GC 还会选择部分老年代 Region 一起回收。
- 不同收集器优化目标不同,需要结合吞吐量、停顿时间、堆大小和业务负载选择。
- CMS 已在较新 JDK 中移除,学习时需要区分 JDK 8 的历史实现与现代 JDK 的实际可用收集器。
十一、本篇面试题与参考答案
1. 垃圾回收算法和垃圾收集器有什么区别?
参考答案:
垃圾回收算法描述的是如何识别和回收垃圾对象的思路,例如标记-清除、标记-复制和标记-整理。
垃圾收集器则是 JVM 中具体执行垃圾回收的实现,它会结合回收算法、内存组织方式、线程调度和具体流程完成垃圾回收。
2. 什么是并行收集和并发收集?有什么区别?
参考答案:
并行收集强调多个 GC 线程同时工作,但回收期间应用线程通常需要暂停。
并发收集强调 GC 线程与应用线程在一段时间内同时运行,可以降低应用程序完全暂停的时间,但仍可能存在 STW 阶段。
3. Serial 和 ParNew 有什么区别?
参考答案:
两者都是新生代收集器,主要采用复制算法的思路。
Serial 使用单个 GC 线程进行回收;ParNew 使用多个 GC 线程并行回收。两者回收时都需要暂停应用线程。
4. Parallel Scavenge 和 ParNew 有什么区别?
参考答案:
两者都是新生代并行收集器,但优化目标不同。
ParNew 主要是为了与 CMS 等老年代收集器配合;Parallel Scavenge 更加关注吞吐量,并支持自适应调节策略,可以根据运行情况调整相关内存参数。
5. CMS 的工作流程是什么?
参考答案:
CMS 主要包含四个阶段:
- 初始标记:STW,标记 GC Roots 直接关联的对象。
- 并发标记:GC 线程与应用线程并发执行,遍历对象图并标记存活对象。
- 重新标记:STW,修正并发标记期间引用变化带来的标记遗漏。
- 并发清除:GC 线程与应用线程并发执行,回收未存活对象。
CMS 主要采用标记-清除算法,因此可能产生内存碎片。
6. CMS 为什么会产生浮动垃圾?
参考答案:
因为 CMS 的并发标记和并发清除阶段,应用线程仍然在运行,对象引用关系可能发生变化,也可能产生新的垃圾对象。
其中一部分在并发清除阶段产生的垃圾,可能无法被本轮回收,只能留到下一次 GC 处理,这部分垃圾称为浮动垃圾。
7. 什么是 CMS 并发模式失败?
参考答案:
CMS 并发回收需要预留一定的老年代空间。如果并发回收还没有完成,应用线程却持续分配对象,导致老年代空间不足,就可能发生 Concurrent Mode Failure。
此时 CMS 可能无法继续按预期完成并发回收,需要触发更重的 Full GC,并可能使用 Serial Old 等后备方式进行回收。
8. G1 为什么使用 Region?
参考答案:
G1 将整个堆划分成多个大小相等的 Region,使内存管理更加灵活。
每个 Region 可以在不同时间承担不同角色,G1 能够根据 Region 中的垃圾比例和预计回收收益,选择合适的 Region 进行回收,从而尽量控制停顿时间。
9. G1 中的 Young GC 和 Mixed GC 有什么区别?
参考答案:
Young GC 主要回收年轻代 Region,将存活对象疏散到其他 Region。
Mixed GC 除了回收年轻代 Region,还会选择部分老年代 Region 一起回收。Mixed GC 通常发生在并发标记完成之后,不等于 Full GC。
10. CMS 和 G1 有什么区别?
参考答案:
CMS 主要采用标记-清除思路,主要针对老年代进行并发回收,能够降低部分 STW 时间,但可能产生内存碎片。
G1 将整个堆划分为多个 Region,通过并发标记和 Region 疏散回收,选择回收收益较高的区域。它能够设置停顿时间目标,并通过复制存活对象改善空间碎片问题。