GC 垃圾回收机制:分代、G1、ZGC

FreeGuideOnline 最新 2026-06-17

自动内存管理基础:为什么需要垃圾回收?

在传统的手动内存管理语言(如 C/C++)中,开发者需要显式地申请与释放内存。一旦忘记释放,就会产生“内存泄漏”;若重复释放或继续使用已释放内存,则会导致程序崩溃。为了解决这类头疼的问题,Java、Go、Python 等语言在运行时引入了垃圾回收(Garbage Collection, GC)机制。

垃圾回收器的核心任务是自动找出不再被程序引用的对象,并回收它们占用的内存。这个“找出”的过程建立在两个经典判定算法之上:

  • 引用计数法:每个对象记录自己被引用的次数,归零时即死亡。但无法解决循环引用问题。
  • 可达性分析法:从一组称为“根”(GC Roots)的引用节点出发,沿着引用链遍历,能被遍历到的对象就是“活着”的,其余皆为垃圾。主流 JVM(HotSpot)使用该方式。

本章将集中讲解 HotSpot JVM 的三种重要垃圾回收方案:分代回收(经典设计基础)G1 GC(现代默认选择)ZGC(超低延迟先锋)

分代回收:让专业的人做专业的事

为何要分代?

大量实践发现,绝大多对象都是“朝生夕死”,只有少数会长期存活。如果对所有对象都使用同一种回收强度,就好比用同一把铲子既挖沙子又挖岩石——效率低下。于是 JVM 把堆内存划分为不同“代”,对新生对象和老旧对象区别对待。

经典分代堆结构

区域 名称 特点 回收频率 常用算法
新生代 Young Generation 新创建的对象优先分配在此,空间较小 频繁 复制算法
老年代 Old Generation 存活时间较长的大对象或从新生代晋升的对象 低频 标记-清除/标记-整理
永久代/元空间 Metaspace 存储类元数据,不属于堆(Java 8 后移除永久代) 按需 -

新生代进一步划分为 Eden 区和两个 Survivor 区(S0、S1):

  • Eden 区:绝大多数新对象诞生地。
  • Survivor 区:经历至少一次 Minor GC 后存活的对象会被复制到此处,并在 S0 和 S1 之间来回挪移。

分配与晋升流程

  1. 新对象首先尝试在 Eden 区分配。
  2. Eden 区满时触发 Minor GC(新生代回收)。
  3. 存活对象从 Eden + 一个 Survivor 区复制到另一个空 Survivor 区,年龄计数器 +1。
  4. 年龄超过阈值(默认 15)的对象晋升到老年代。
  5. 若老年代空间不足,触发 Full GC(整堆回收),通常伴随着“Stop-The-World”停顿。

分代回收的代表收集器有 Serial、Parallel Scavenge、Parallel Old 以及经典的 CMS(并发标记清除)。它们为理解 G1 和 ZGC 提供了基石。

G1 GC:化整为零的平衡艺术

设计目标与核心思想

G1(Garbage-First)从 JDK 7 开始引入,JDK 9 成为默认回收器。它的设计目标是在较大堆内存(数 GB 到数十 GB)上提供可预测的停顿时间,同时兼顾高吞吐量。

它彻底打破了连续分代的物理布局,转而将堆划分为大量大小相等的 Region(默认约 2MB)。每个 Region 可充当 Eden、Survivor、Old 或 Humongous(存放大对象)的角色。Region 化使得回收粒度从“整代”下降到“一小块区域”。

核心改进:Mixed GC 与停顿时间模型

G1 的回收周期主要分为两种类型:

  • Young GC:只回收全新生代的 Region,类似分代中的 Minor GC。触发时把 Eden 区存活对象物复制到 Survivor 区。
  • Mixed GC:在足够进行多次 Young GC 后,G1 会选择一部分回收收益最高的 Old 区域进行收集,将这些区域里的存活对象整理并复制到其它 Old Region,从而在老年代出现碎片前就进行压缩。

G1 能够实现软实时停顿的关键在于它的停顿预测模型。G1 会根据每个区域的历史回收时间与存活对象数量,计算出收集哪些 Region 可以在指定的停顿目标(如 -XX:MaxGCPauseMillis=200)内完成。每次只回收一小部分老年代 Region,把大任务拆解为零碎的“小步快跑”。

并发标记与 SATB

G1 使用 Snapshot-At-The-Beginning(SATB) 算法进行并发标记。它在标记开始时拍下一张堆“快照”,即使之后引用发生变更,也能通过一个称为 Remembered Set(RSet) 的结构快速获知哪些 Region 引用了本 Region 的对象。这样,标记阶段就可以很大程度与用户线程并发执行,只在一开始和最后需要短暂的 Stop-The-World。

RSet 本身会占用额外内存(约堆的 5% 甚至更高),这也是 G1 在高堆内存场景下需要调优的要点之一。

ZGC:亚毫秒级延迟的革命

为什么需要 ZGC?

即便 G1 将停顿控制在百毫秒级别,对于响应时间敏感的服务(如交易系统、游戏服务器)依然可能造成毛刺。ZGC(Z Garbage Collector)从 JDK 11 开始实验性提供,JDK 15 起正式生产可用,它的终极目标是无论堆内存多大(128MB~16TB),停顿时间始终低于 10ms,且不会随堆大小或存活对象量的增长而显著增加

染色指针:不可思议的魔法

ZGC 的核心革新是染色指针技术。常规 64 位系统上,指针的高 16 位未被完全利用,ZGC 在这些空闲比特中嵌入了 GC 状态信息(Marked、Remapped 等)。当访问一个对象时,只需读取指针上的颜色信息,即可快速判定该对象是否已被标记、转发。

这带来了三大直接好处:

  • 并发整理:移动对象时无需像传统 GC 那样写屏障来保护,因为指针本身记录了对象是否在移动。
  • 极低的停顿:几乎所有阶段都可以完全并发,只有根扫描等极小部分需要暂停。
  • 无碎片化:Region 被整理后完全消除碎片。

工作阶段一览

ZGC 的一次回收周期分为多个阶段,且大部分与用户线程并发:

  1. 暂停—根标记:极短的 Stop-The-World,扫描并标记根对象。
  2. 并发标记:遍历对象图,为存活对象染色。
  3. 暂停—再标记:处理并发标记期间缺失的少量对象。
  4. 并发准备与转移:决定哪些 Region 需要压缩,移动对象并更新指针。
  5. 暂停—初始转移:另一个极短的暂停,确认迁移信息。
  6. 并发转移:真正的对象复制发生在并发阶段,利用读屏障在访问时自愈指针。

适用场景与调优

ZGC 特别适合超大堆、低延迟敏感的现代 Java 应用。但它会消耗更多 CPU 资源来维护染色指针和读屏障,因此对吞吐量略有影响。通过 -XX:+UseZGC 开启后,一般只需关注并发线程数(-XX:ConcGCThreads)以及最大堆设置。它不需要像 G1 那样精细调整停顿时间目标,因为本身停顿就已极低。

总结:如何选择适合你的 GC?

  • 堆较小(<4GB)、停顿要求不严格:传统分代收集器(如 Parallel GC)可提供最高吞吐量。
  • 堆较大、希望停顿控制在百毫秒级、需要可预测性:使用 G1 GC,并通过 MaxGCPauseMillis 设置合理目标。
  • 堆很大(>16GB)或追求极低停顿(<10ms):ZGC 是最佳选择,尤其适用于 JDK 15+ 生产环境。
  • 仍在使用 JDK 8 且内存不超 4GB:不一定非要迁移 G1,结合实际情况评估。

垃圾回收器的演进正是对“吞吐量”与“响应时间”这对矛盾的持续平衡。从分代到 G1 再到 ZGC,每一步都让 Java 在分布式高并发系统中变得更加可靠。理解其内部原理后,你可以通过 java -XX:+PrintCommandLineFlags -version 查看当前默认收集器,并用 jstat -gcutil 等工具观察实际回收行为,从而做出最适合业务的选择。