跳到主要内容

JVM GC 知识点

· 阅读需 8 分钟

JVM 中常见的垃圾收集器主要包括 Serial、Parallel、ParNew、CMS、G1 以及 ZGC 等。不同收集器在 并发能力、停顿时间、吞吐量以及适用场景 上各不相同。下面对常见的几种垃圾收集器进行整理说明。

Serial 收集器

对应参数:-XX:+UseSerialGC -XX:+UseSerialOldGC

Serial 收集器是 JVM 中最基础、历史最悠久的垃圾收集器,它采用 单线程方式执行垃圾回收。在进行垃圾回收时,JVM 会触发 STW(Stop The World),即暂停所有用户线程,直到垃圾回收完成后才恢复业务线程运行。

在算法实现上,Serial 收集器在 新生代使用标记-复制算法(Copying),而在 老年代使用标记-整理算法(Mark-Compact)。由于整个回收过程只有一个 GC 线程,因此实现简单且稳定,但在堆内存较大或 CPU 核心较多的情况下,垃圾回收效率会明显下降。

因此 Serial 收集器通常适用于 内存规模较小、CPU 核心较少的应用环境,例如早期的 Client 模式 JVM 或一些资源受限的系统。

Parallel Scavenge 收集器

对应参数:-XX:+UseParallelGC -XX:+UseParallelOldGC

Parallel Scavenge 收集器是 Serial 收集器的 多线程版本,它的核心目标是 提高系统吞吐量(Throughput)。在垃圾回收时,会使用多个 GC 线程并行执行,从而提高整体回收效率。

默认情况下,GC 线程数量与 CPU 核数基本一致,也可以通过参数 -XX:ParallelGCThreads 手动指定线程数量。Parallel 收集器在算法上与 Serial 收集器基本一致,即 新生代使用标记-复制算法,老年代使用标记-整理算法

由于 Parallel 收集器更关注 吞吐量而非停顿时间,因此非常适合 后台计算型服务或批处理系统。在一些堆内存规模中等(例如 3~4GB 左右)的应用场景中,该收集器可以获得较好的性能表现。

ParNew 收集器

对应参数:-XX:+UseParNewGC

ParNew 收集器可以理解为 Serial 收集器的多线程版本,但它的主要用途并不是单独使用,而是 与 CMS 收集器配合使用。ParNew 负责 新生代的垃圾回收,而 CMS 则负责 老年代的垃圾回收

在实现方式上,ParNew 采用 多线程并行回收机制,并且在新生代中仍然使用 标记-复制算法。由于 CMS 是一个以低停顿时间为目标的收集器,而 Serial 收集器是单线程的,因此在实际使用 CMS 时通常会搭配 ParNew 来提高新生代回收效率。

这种组合(ParNew + CMS)在 早期 Web 服务和对停顿时间较敏感的系统中使用较多

CMS 收集器

对应参数:-XX:+UseConcMarkSweepGC

CMS(Concurrent Mark Sweep)收集器主要用于 老年代垃圾回收,其设计目标是 尽可能减少垃圾回收时的停顿时间。CMS 的一个重要特点是 允许 GC 线程与用户线程并发执行,从而减少应用暂停时间。

CMS 使用 标记-清除算法(Mark-Sweep),并基于 三色标记法 来完成对象可达性分析。整个回收过程通常分为五个阶段:

  1. 初始标记(Initial Mark) 触发 STW,仅标记 GC Roots 直接引用的对象,由于对象数量较少,因此执行速度很快。
  2. 并发标记(Concurrent Mark) 与用户线程并发执行,遍历整个对象图,标记所有可达对象。
  3. 重新标记(Remark) 再次触发 STW,用于修正并发标记期间由于对象引用变化而产生的偏差。
  4. 并发清理(Concurrent Sweep) 与用户线程并发执行,清理未被标记的对象。
  5. 并发重置(Concurrent Reset) 重置 CMS 的内部状态,为下一次 GC 做准备。

CMS 流程图:

CMS 把回收过程从一个步骤拆分为多个步骤,标记和清理都做了拆分,并把最耗时的标记对象步骤放到与用户线程并发执行,从而让应用程序几乎感知不到 STW 的停顿,只有初始标记与重新标记两个阶段存在很短的 STW,其余流程都是并发进行的。所以 CMS 达到了 最短回收停顿 的目标。不过最短停顿时间不代表整个回收流程耗时更短,实际上 CMS 的整体回收时间是要大于其他垃圾收集器的,只是它的停顿时间是最小的。

G1 收集器

对应参数:-XX:+UseG1GC

G1(Garbage First)收集器是 JDK9 之后默认的垃圾收集器,主要用于 大内存服务器环境。与传统垃圾收集器按照新生代和老年代进行物理划分不同,G1 将整个堆划分为多个 Region(区域)

每个 Region 可以根据需要动态地作为 Eden、Survivor 或 Old 区域 使用。G1 会统计每个 Region 中的垃圾比例,并优先回收 垃圾最多、回收收益最高的 Region,这也是 "Garbage First" 名称的来源。

G1 的另一个重要特点是 可预测的停顿时间模型。通过参数 -XX:MaxGCPauseMillis 可以设定期望的最大停顿时间,G1 会根据该目标自动调整回收策略。

整体回收流程包括 初始标记、并发标记、最终标记以及 Region 回收 等阶段。相比 CMS,G1 在减少内存碎片、支持大堆内存以及控制停顿时间方面表现更加优秀,因此在现代 JVM 环境中被广泛使用。

ZGC 收集器

ZGC 是 JDK11 引入的一种低延迟垃圾收集器,其核心目标是 实现毫秒级的 GC 停顿时间。在大多数情况下,ZGC 的 STW 停顿时间可以控制在 10ms 以内,即使在 TB 级别堆内存下也能保持稳定。

ZGC 的实现依赖于几项关键技术,包括 染色指针(Colored Pointer)读屏障(Load Barrier) 以及基于 Region 的 ZPage 内存管理机制。通过这些技术,ZGC 可以在大部分时间内与用户线程并发执行垃圾回收。

ZGC 的主要回收过程包括 并发标记、并发重定位以及并发引用修复。由于绝大多数工作都在并发阶段完成,因此对应用线程的影响非常小。

因此 ZGC 非常适用于 超大内存服务器、实时系统以及金融交易等对延迟极其敏感的应用场景

常见 GC 收集器对比

  • Serial:单线程回收,适合小型应用
  • Parallel:多线程回收,吞吐量优先
  • ParNew + CMS:低停顿方案(JDK8 常见)
  • G1:现代 JVM 默认收集器,适合大内存服务
  • ZGC:超低延迟垃圾收集器,适合 TB 级内存系统
收集器类型线程模式主要算法停顿特点适用场景
Serial新生代 + 老年代单线程复制 + 标记整理STW 时间较长小型应用、单核 CPU
Parallel Scavenge新生代多线程复制算法吞吐量优先后台计算、批处理任务
Parallel Old老年代多线程标记整理吞吐量优先高吞吐服务
ParNew新生代多线程复制算法停顿较短配合 CMS 使用
CMS老年代并发标记清除低停顿Web 服务、低延迟系统
G1全堆(Region)并发 + 并行复制 + 标记整理可预测停顿大内存服务器
ZGC全堆(Region)高度并发标记 + 重定位<10ms 停顿超大堆内存、低延迟系统

扩展:Golang GC 基本机制

Go 从 1.5 版本开始引入三色标记并发 GC(Tri-color Mark and Sweep),并在后续版本持续优化,目前已经形成一套成熟的 并发标记 + 并发清理 + 写屏障(Write Barrier) 的 GC 体系。

Golang GC 的核心特点包括:

  • 三色标记算法(Tri-color Marking)
  • 并发标记(Concurrent Mark)
  • 写屏障(Write Barrier)保证对象引用一致性
  • STW 时间极短(通常 < 1ms)
  • 自动触发 GC(基于堆增长比例)

Golang GC 的主要流程如下:

  1. STW Start:暂停所有 Goroutine,初始化 GC Root。
  2. 并发标记(Concurrent Mark):GC 与用户 Goroutine 并发执行,使用三色标记遍历对象引用。
  3. 标记终止(Mark Termination):短暂 STW,确保所有对象标记完成。
  4. 并发清理(Concurrent Sweep):回收未标记对象,与程序并发运行。

JVM GC 与 Golang GC 的核心差异

JVM 采用 多种垃圾收集器(Serial、CMS、G1、ZGC 等)并支持分代回收机制,通过不同算法与参数组合来适配不同场景;而 Golang 从设计之初就采用统一的三色标记并发 GC(Tri-color Mark & Sweep)体系,不做分代,通过并发标记、写屏障和极短 STW 来实现低延迟回收,因此整体结构更简单、调优成本更低。

为什么 Go 不做分代 GC?因为 Go 团队认为:分代 GC 对延迟敏感系统收益不明显,但会增加运行时复杂度。

小结

从 Serial 到 ZGC,JVM 垃圾收集器的演进主线其实就是在吞吐量和停顿时间之间不断权衡。单线程的 Serial 胜在简单稳定,Parallel 追求吞吐,ParNew + CMS 用并发换来了低停顿,G1 通过 Region 化把停顿变得可预测,ZGC 则借助染色指针和读屏障把停顿压到了毫秒级。选择收集器时不必追新,关键还是看应用的堆规模和延迟要求。对比 Golang 统一且不分代的 GC 设计,也能更清楚地看到两种运行时在复杂度与灵活性上的不同取舍。

评论 / COMMENTS