GC 的常见算法和垃圾收集器

6 阅读7分钟

一、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 则更适合低延迟、大内存场景。