深度解析 G1 垃圾收集器:化整为零,打破延迟与吞吐量的博弈
在 JDK 1.7 时代作为重要进化特征登场,并最终在 JDK 9 中成功上位成为默认垃圾收集器,G1(Garbage-First)无疑是 JVM 垃圾回收史上的一个巨大里程碑。
G1 是一款专为配备多颗处理器及大容量内存的服务端机器量身打造的收集器。它的终极目标是:在极高概率满足设定的 GC 停顿时间要求的同时,依然维持高吞吐量的性能特征。 要想彻底搞懂 G1,我们需要从它的四大核心特点、特殊的内存布局,以及它独有的两种回收模式来层层剖析。
一、 G1 的四大核心基石
G1 能够取代 CMS,主要归功于它在设计上的四个维度的全面进化:
- 并行与并发:G1 能充分压榨多核 CPU 的硬件优势。在需要 STW(Stop-The-World)的阶段,它使用多个 GC 线程并行工作来缩短停顿时间;而在耗时最长的标记阶段,G1 能让 GC 线程与 Java 用户线程并发执行。
- 保留分代收集,但打破物理隔离:G1 不需要其他收集器配合就能独立管理整个堆内存。它依然保留了新生代(Eden/Survivor)和老年代的概念,但在物理上,这些代不再是连续的内存块。
- 空间整合(告别内存碎片):
- 从整体看:G1 是基于“标记-整理”算法实现的,不会产生内存碎片。
- 从局部看(两个 Region 之间):它是基于“标记-复制”算法实现的。这就保证了 G1 运行期间能提供规整的可用内存,极大降低了因为分配大对象找不到连续空间而被迫触发 Full GC 的概率。
- 可预测的停顿模型(最大杀手锏):G1 允许使用者明确指定一个目标:期望每次垃圾回收的最大暂停时间不能超过 N 毫秒(通过参数
-XX:MaxGCPauseMillis=n配置,默认 200ms)。G1 会在后台建立时间预测模型,尽可能地保证在这个时间限制内完成回收。
二、 内存布局:化整为零的 Region 设计
为了实现“可预测的停顿”,G1 彻底颠覆了传统的内存布局。
它将整个 Java 堆划分为了数千个大小相等的独立区域——Region。 每一个 Region 都可以根据需要,动态地扮演 Eden 区、Survivor 区 或 Old(老年代)区。此外,G1 还专门设计了一种 Humongous 区,用于专门存储体积庞大的大对象(大小超过一个 Region 容量 50% 的对象)。
这种“把内存化整为零”的设计,让 G1 能够以 Region 为最小回收单元,避免了每次都去扫描和清理整个堆。
三、 G1 的两种核心回收模式
结合文档中的深入剖析,G1 实际的运行绝不是简单的“一刀切”,而是主要分为两种回收方式:Young GC(年轻代回收) 和 Mixed GC(混合回收)。
1. Young GC(年轻代回收)
当新生代(主要是 Eden 区)的内存占比达到阈值(最大默认 60%),无法再为新对象分配空间时,就会触发 Young GC。这个过程会发生短暂的 STW。
微观执行流程:
- 触发与标记:新创建的对象都在 Eden 区。触发 GC 后,G1 首先会标记出 Eden 和 Survivor 区域中的所有存活对象。
- 动态筛选与复制:这是 G1 最智能的地方!G1 在后台会记录以往每次回收 Eden 和 Survivor 区的平均耗时。
举个硬核的例子: 假设你配置了
-XX:MaxGCPauseMillis=200(最大停顿 200ms),而 G1 历史统计得出回收一个 Region 平均需要 40ms。那么 G1 经过精确计算,这次回收最多只能回收 4 个 Region(40ms * 4 = 160ms < 200ms),以保证不违背停顿时间的承诺。
- 转移与清空:根据计算出的数量,G1 选择部分 Region,将里面的存活对象复制到一个新的 Survivor 区中(并且对象年龄 +1),然后直接清空原来的旧 Region 区域。后续的 Young GC 会不断重复这个过程,直到对象年龄达标晋升到老年代。
2. 混合回收(Mixed GC)的四大步骤
当老年代的堆空间占用达到一定阈值(默认 45%)时,会触发并发标记周期,并最终进入 Mixed GC 阶段。Mixed GC 不仅回收整个年轻代,还会回收部分老年代。
它的运作大致分为以下四个经典步骤:
- 初始标记(Initial Mark):短暂停顿(STW)。仅仅标记从 GC Roots 能直接引用的活跃对象。由于通常借用 Young GC 的 STW 同步完成,所以速度极快。
- 并发标记(Concurrent Mark):与应用并发运行。从 GC Roots 的直接引用开始,扫描整个堆,标记所有可达对象。耗时较长,但不会阻塞用户线程。
- 最终标记(Final Mark / Remark):短暂停顿(STW)。处理并发标记期间,因为用户线程继续运行而导致引用关系发生变化的残留对象(G1 这里使用的是 SATB 算法)。
- 筛选回收(Cleanup / Evacuation):短暂停顿(STW)。这是体现 Garbage-First(垃圾优先) 名字由来的核心阶段!
- G1 会根据之前并发标记的结果,计算每个 Region 的垃圾占比。
- 它在后台维护了一个优先列表,根据用户允许的收集时间(
MaxGCPauseMillis),优先选择回收价值最大(垃圾最多、存活对象最少)的老年代 Region 进行回收。 - 把选中 Region 中的存活对象复制到新区域,然后清空旧区域。
四、 G1 的代价与局限性
世界上没有免费的午餐,G1 为了实现这种高效率和可预测性,也付出了不小的底层代价:
- 极高的内存占用:为了解决 Region 之间的跨代引用问题,每个 Region 必须维护一个叫 Remembered Set (RSet) 的数据结构,记录“谁引用了我”。在堆被切成几千个 Region 的情况下,G1 收集器维护这些 RSet 需要消耗堆内存容量的 10% ~ 20%。
- CPU 负载负担:为了在并发运行时维护复杂的 RSet 更新,G1 在给对象赋值的底层操作中加入了写屏障(Write Barrier),这些额外的计算逻辑会消耗更多的 CPU 资源。
总结: G1 的出现,完美诠释了“以空间换时间”和“化整为零”的架构哲学。它通过精确掌控 Region 的回收收益和耗时,让 Java 终于拥有了一款在吞吐量与低延迟之间取得极佳平衡的现代垃圾收集器。