一、GC 的常见算法
GC 的核心目标就是:识别垃圾对象,并回收可用内存。
1. 标记-清除(Mark-Sweep)
过程
分两步:
- 标记:先把需要回收的对象标记出来
- 清除:把这些垃圾对象占用的内存回收掉
优点
- 实现简单
- 不需要移动存活对象
缺点
- 效率一般
- 会产生内存碎片
适用场景
- 老年代中有时会用到这种思想
2. 复制算法(Copying)
过程
把内存分成两块,每次只使用其中一块。
发生 GC 时,把还存活的对象复制到另一块,然后把原来这一整块一次性清空。
优点
- 实现简单
- 不会产生内存碎片
- 复制后内存连续,分配对象快
缺点
- 内存利用率低,通常只能用一半
- 如果存活对象多,复制成本高
适用场景
- 新生代
- 因为新生代对象大多“朝生夕死”,存活率低,复制成本小
3. 标记-整理(Mark-Compact / Mark-整理)
过程
- 先标记存活对象
- 然后把存活对象往一端移动
- 最后清理边界外的内存
优点
- 没有内存碎片
- 比标记-清除更适合对象存活率高的区域
缺点
- 需要移动对象,成本比标记-清除高
- 移动对象时需要更新引用
适用场景
- 老年代
4. 分代收集(Generational Collection)
这不是一种具体基础算法,而是一种组合思想,也是 HotSpot 最常见的设计方式。
核心思想
根据对象存活特点把堆分代:
- 新生代:对象存活率低,适合复制算法
- 老年代:对象存活率高,适合标记-清除或标记-整理
- 有些实现还会考虑元空间等区域
为什么要分代
因为不同区域对象特点不同,使用同一种算法不划算。
二、如何判断对象是不是垃圾
在讲收集器前,最好顺带提一句这个,面试官常追问。
1. 引用计数法
给对象维护一个引用计数,引用加一,失效减一;计数为 0 就是垃圾。
优点
- 直观
- 判定效率高
缺点
- 无法解决循环引用问题
因此 Java 主流 JVM 不采用它作为主要垃圾判断方式。
2. 可达性分析算法
Java 主要采用这个。
从一组 GC Roots 出发向下搜索,能被引用链到达的对象就是存活对象;到达不了的就是垃圾。
常见 GC Roots
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈 JNI 引用的对象
- 活跃线程对象等
三、常见垃圾收集器
HotSpot 里常见的垃圾收集器,可以按“新生代 / 老年代 / 整堆”来理解。
1. Serial 收集器
特点
- 单线程
- 进行 GC 时会 Stop The World
适用区域
- 新生代:
Serial - 老年代:
Serial Old
使用算法
- 新生代:复制算法
- 老年代:标记-整理
优点
- 简单高效
- 单核或小内存场景下开销小
缺点
- 暂停时间长
- 不适合大内存、多核服务器
2. ParNew 收集器
特点
- Serial 的多线程版本
- 新生代并行收集器
- 也会 Stop The World
使用算法
- 复制算法
特点补充
- 曾经常和 CMS 搭配使用
- 在较新的 JDK 中实际使用已经很少了
3. Parallel Scavenge 收集器
特点
- 新生代并行收集器
- 关注点是 吞吐量
- 适合后台计算、批处理任务
使用算法
- 复制算法
关键点
吞吐量 = 用户代码运行时间 / (用户代码运行时间 + GC 时间)
优点
- 高吞吐量
- 可配合自适应调节策略
缺点
- 不以最短停顿时间为目标
4. Parallel Old 收集器
特点
- Parallel Scavenge 的老年代版本
- 多线程并行收集
使用算法
- 标记-整理
适用
- 注重吞吐量的场景
5. CMS(Concurrent Mark Sweep)收集器
这是面试里特别高频的一个。
特点
- 以 最短停顿时间 为目标
- 老年代收集器
- 大部分阶段可以与用户线程并发执行
主要过程
- 初始标记(STW)
- 并发标记
- 重新标记(STW)
- 并发清除
使用算法
- 标记-清除
优点
- 停顿时间短
- 适合对响应时间敏感的系统,如 Web 服务
缺点
- 对 CPU 资源敏感
- 会产生内存碎片
- 并发清理阶段会产生“浮动垃圾”
- 失败时可能触发 Serial Old,导致长时间停顿
状态
- 在后续 JDK 中已被逐步淘汰,通常被 G1 替代
6. G1(Garbage First)收集器
这个现在非常高频,尤其 JDK 8 以后。
核心思想
G1 不再简单按固定的新生代、老年代连续分区管理整个堆,而是把堆划分为多个 Region。
特点
- 面向 服务端
- 兼顾 吞吐量 和 低停顿
- 可以预测停顿时间
- 整体上是 标记-整理 思想,局部 Region 间回收类似复制
回收过程(简化)
- 初始标记
- 并发标记
- 最终标记
- 筛选回收 / Evacuation
优点
- 降低内存碎片
- 可控停顿
- 适合大堆内存
缺点
- 实现复杂
- 在某些小堆场景下未必比其他收集器更划算
为什么叫 Garbage First
优先回收“垃圾最多、收益最大”的 Region。
7. ZGC
特点
- 低延迟垃圾收集器
- 停顿时间非常短
- 适合超大堆和低延迟场景
核心特点
- 并发标记
- 并发搬迁
- 染色指针、读屏障等技术
优点
- 停顿时间通常非常低
- 堆很大时表现也好
缺点
- 相对更复杂
- 适合对延迟要求高的场景,不一定是所有系统首选
8. Shenandoah
也是低停顿收集器,和 ZGC 类似方向。
特点
- 更强调并发压缩和低停顿
- 目标也是尽量降低 STW 时间
场景
- 对响应延迟敏感的大堆应用
四、收集器总结表
| 收集器 | 作用区域 | 线程 | 目标 | 算法/特点 |
|---|---|---|---|---|
| Serial | 新生代 | 单线程 | 简单 | 复制算法 |
| Serial Old | 老年代 | 单线程 | 简单 | 标记-整理 |
| ParNew | 新生代 | 多线程 | 响应较好 | 复制算法 |
| Parallel Scavenge | 新生代 | 多线程 | 高吞吐量 | 复制算法 |
| Parallel Old | 老年代 | 多线程 | 高吞吐量 | 标记-整理 |
| CMS | 老年代 | 并发 | 低停顿 | 标记-清除 |
| G1 | 整堆 | 并发+并行 | 可预测停顿 | Region,整体整理 |
| ZGC | 整堆 | 并发 | 超低停顿 | 并发标记/搬迁 |
| Shenandoah | 整堆 | 并发 | 低停顿 | 并发压缩 |
五、面试里怎么回答更好
GC 常见基础算法有标记-清除、复制、标记-整理,以及分代收集思想。新生代因为对象存活率低,通常采用复制算法;老年代因为对象存活率高,通常采用标记-清除或标记-整理。垃圾对象识别主要通过可达性分析,而不是引用计数。常见垃圾收集器有 Serial、ParNew、Parallel Scavenge、Parallel Old、CMS、G1,以及更偏低延迟的 ZGC、Shenandoah。其中 CMS 以低停顿为目标,但有内存碎片问题;G1 通过 Region 化管理堆,兼顾吞吐量和停顿时间;ZGC 和 Shenandoah 更适合超低延迟场景。
六、最容易被追问的点
1. CMS 和 G1 的区别
- CMS:老年代收集器,标记-清除,低停顿,但会产生碎片
- G1:面向整个堆,Region 化,能预测停顿时间,碎片更少
2. 为什么新生代适合复制算法
- 因为大多数对象很快死亡
- 每次需要复制的存活对象少
- 回收效率高
3. 为什么老年代不用复制算法
- 老年代对象存活率高
- 如果还用复制算法,复制成本太高
- 还需要预留大量额外空间,不划算
4. STW 是什么
Stop The World- 垃圾回收时让用户线程暂停
- 几乎所有收集器都会有,只是停顿时间长短不同
七、背诵版
GC 的常见算法主要有标记-清除、复制、标记-整理,以及分代收集思想。Java 判断对象是否可回收主要采用可达性分析算法。新生代对象存活率低,通常采用复制算法;老年代对象存活率高,通常采用标记-清除或标记-整理。常见垃圾收集器有 Serial、ParNew、Parallel Scavenge、Parallel Old、CMS、G1、ZGC 和 Shenandoah。Serial 和 Parallel 系列更偏向吞吐量,CMS 以低停顿为目标但有内存碎片问题,G1 通过 Region 化管理堆,兼顾吞吐量与停顿时间,ZGC 和 Shenandoah 则更适合低延迟、大内存场景。