JVM(12)——详解G1垃圾回收器
G1(Garbage-First)垃圾回收器。它是现代 Java 应用中默认的垃圾回收器(自 JDK 9 起),旨在提供一个高性能、可预测停顿时间(低延迟)的解决方案,尤其适合大内存(多GB甚至TB级别)和多核处理器的服务器环境。
一、G1 的设计目标与定位
-
取代 CMS 和 Parallel Scavenge:
-
CMS(Concurrent Mark-Sweep)虽然低延迟,但存在内存碎片化、无法处理浮动垃圾导致 Full GC 等问题,且对堆大小有限制。
-
Parallel Scavenge(吞吐量优先)追求高吞吐量,但无法提供可预测的停顿时间。
-
-
核心目标:
-
可预测的停顿时间模型: 允许用户指定期望的最大 GC 停顿时间(
-XX:MaxGCPauseMillis
,默认 200ms),G1 会尽力满足这个目标。 -
高吞吐量: 在满足停顿时间目标的前提下,尽可能提高应用吞吐量。
-
适应大堆: 能够高效地管理从几百MB到几十TB大小的堆内存。
-
-
关键特性:
-
并发与并行: 利用多核优势,在应用线程运行的同时执行部分 GC 工作(并发),并在需要停顿的阶段使用所有可用 CPU 资源并行处理(并行)。
-
分代收集: 仍然遵循分代假说(大多数对象朝生夕死),但物理上不再严格划分为连续的年轻代和老年代区域。
-
空间整合: 使用复制算法进行回收,有效避免 CMS 带来的内存碎片问题。
-
可扩展性: 设计上能更好地利用现代多核 CPU 和大内存硬件。
-
二、核心概念:Region 化堆 (Heap Regionization)
这是 G1 区别于之前回收器的最根本设计。
-
堆划分: G1 将整个 Java 堆划分为多个大小相等(默认约 1MB 到 32MB,具体大小根据堆初始值和最大值的平均值除以约 2048 决定)的独立内存区域,称为 Region。
-
Region 类型: 每个 Region 在任意时刻都被赋予一个特定的角色:
-
Eden Region: 存放新创建的对象(属于年轻代)。
-
Survivor Region: 存放年轻代 GC 后存活的对象(属于年轻代)。
-
Old Region: 存放存活时间较长、经过多次年轻代 GC 后仍然存活的对象(属于老年代)。
-
Humongous Region: 专门用于存储巨型对象(大小超过单个 Region 容量 50% 的对象)。一个巨型对象可能占用一个或多个连续的 Humongous Region。
-
空闲 Region: 未分配的可用空间。
-
-
动态角色转换: Region 的角色不是固定的。在一次 GC 后,一个 Eden Region 被清空后可以变成 Survivor 或 Old Region,或者被释放为空闲 Region。一个 Survivor Region 在对象晋升后可以变成 Old Region。
-
优势:
-
灵活的堆管理: 不再需要预先固定年轻代和老年代的大小比例,允许根据回收效率和停顿目标动态调整各代占用的 Region 数量。
-
可预测停顿的基础: GC 的工作可以集中在包含最多垃圾(Garbage-First 名字的由来)的 Region 集合上,而不是扫描整个堆或整个老年代,使得每次回收的时间更可控。
-
增量回收: 每次 GC 可以只处理一部分 Region。
-
三、核心数据结构:Remembered Sets (RSet) 和 Collection Sets (CSet)
-
Remembered Set (RSet):
-
目的: 解决跨 Region 引用问题。一个 Region 中的对象可能被其他 Region 中的对象引用(跨代引用或同代不同 Region 引用)。为了避免在回收一个 Region 时扫描整个堆来确定对象是否存活,G1 为每个 Region 维护一个 RSet。
-
原理: RSet 本质上是一个指向该 Region 的“外来指针”的集合(更准确地说,是记录哪些其他 Region 包含指向本 Region 的引用)。它是一个Points-Into 结构。
-
实现: 通常使用哈希表或卡表(Card Table)的变体实现,粒度比传统卡表更细(可能是卡页的一部分)。
-
维护: 写屏障(Write Barrier)负责检测应用线程对对象引用的写操作(如
obj.field = otherObj
)。如果这个写操作导致了一个跨 Region 的引用(例如,obj
在 Region A,otherObj
在 Region B),那么写屏障会将被修改的引用所在的卡页(或更细粒度)标记为“脏”,并将相关信息记录到otherObj
所在 Region B 的 RSet 中(表示 Region A 有对象指向 Region B)。 -
重要性: RSet 是 G1 实现部分收集(Partial GC) 的关键。当回收某个 Region 时,只需扫描该 Region 内部的对象和其 RSet 中记录的引用来源 Region,即可确定该 Region 中对象的存活状态,避免扫描整个堆,大大减少了 GC 的工作量。
-
-
Collection Set (CSet):
-
目的: 定义在一次 GC 周期(通常是 Young GC 或 Mixed GC)中哪些 Region 将被回收。
-
选择策略:
-
年轻代回收: CSet 包含所有 Eden Region 和 Survivor Region(即整个年轻代)。
-
混合回收: CSet 包含所有年轻代 Region(Eden + Survivor)加上根据预测模型和停顿时间目标精心挑选出的一部分垃圾比例高(Garbage-First)的老年代 Region。
-
-
决策依据: G1 根据 Region 的垃圾比例(可回收空间)、回收所需时间(复制存活对象的成本)以及用户设置的
MaxGCPauseMillis
目标,动态计算并选择一组 Region 加入 CSet,使得回收这组 Region 的预期时间接近但不超出目标停顿时间。 -
意义: CSet 是实现可预测停顿的核心机制。通过控制每次回收处理的 Region 集合的大小(垃圾量),G1 能够控制每次 GC 停顿的时长。
-
四、G1 的 GC 周期 (Garbage Collection Cycle)
G1 的 GC 活动不再严格区分 Minor GC 和 Major GC,而是组织成一个持续的、由不同阶段组成的周期:
-
年轻代回收 (Young-only Phase):
-
触发条件: 当 Eden Region 被填满时触发。
-
过程:
-
Stop-The-World (STW): 暂停所有应用线程。
-
根扫描: 从 GC Roots(栈、寄存器、全局变量、JNI 引用等)开始扫描。
-
更新/处理 RSet: 处理在并发标记阶段记录的引用变化(SATB 缓冲区)。
-
对象拷贝: 并行地将 CSet(所有 Eden + Survivor Region)中的存活对象复制到新的 Survivor Region 或(如果对象年龄达到阈值
-XX:MaxTenuringThreshold
)直接晋升到 Old Region。复制过程使用复制算法。 -
清空 CSet: 回收的 Region 被清空并放回空闲 Region 列表。
-
调整 Survivor 数量: 根据停顿目标动态调整下次 Young GC 使用的 Survivor Region 数量。
-
-
特点: 只回收年轻代 Region。频繁发生,但通常停顿时间较短。
-
-
并发标记周期 (Concurrent Marking Cycle):
-
触发条件: 当整个堆的使用率超过一个阈值(
-XX:InitiatingHeapOccupancyPercent
, IHOP, 默认 45%)时启动。这个周期为后续的混合回收识别出老年代中垃圾比例高的 Region。 -
阶段 (部分并发,部分 STW):
-
初始标记 (Initial Mark - STW): 短暂停顿,标记所有从 GC Roots 直接可达的对象。通常借道一次 Young GC 完成(因为 Young GC 本身就需要扫描根)。
-
根区域扫描 (Root Region Scanning - Concurrent): 扫描 Survivor Region(作为根区域,因为它们可能引用老年代对象),完成后才能开始下一次 Young GC。
-
并发标记 (Concurrent Marking - Concurrent): 与应用线程并发执行,遍历对象图,标记所有可达(存活)对象。使用 SATB (Snapshot-At-The-Beginning) 算法保证标记一致性:在标记开始时对对象图做逻辑快照,标记过程中新分配的对象视为存活,并发期间删除的引用通过写屏障记录在 SATB 缓冲区,在后续阶段处理。
-
最终标记 (Remark - STW): 短暂停顿,处理 SATB 缓冲区,完成标记过程。进行全局引用处理(如类卸载准备)和弱引用处理。
-
清理 (Cleanup - STW & Concurrent):
-
STW 部分: 统计每个 Region 中存活对象的数量,排序 Region(根据可回收空间),识别完全空闲的 Region 并回收。为混合回收选择初始的候选老年代 Region集合。
-
Concurrent 部分: 执行实际的内存回收(如重置空 Region)和 RSet 清理。
-
-
-
产出: 得到一份老年代 Region 的垃圾比例排序列表,供混合回收使用。
-
-
混合回收 (Mixed Collection Phase):
-
触发条件: 在并发标记周期完成后立即开始(如果老年代中有足够多的可回收 Region)。
-
过程: 类似于 Young GC,但是 CSet 不仅包含所有年轻代 Region(Eden + Survivor),还包含一部分(根据停顿目标选择)在并发标记周期中识别出的垃圾比例高的老年代 Region。
-
特点:
-
在一次 STW 停顿中,同时回收年轻代和部分老年代。
-
可能会有多次连续的 Mixed GC,直到回收了足够多的老年代空间(达到 IHOP 阈值以下,或没有足够高垃圾比例的老年代 Region 了)。
-
这是 G1 回收老年代的主要方式,避免了 Full GC。
-
-
-
Full GC (Serial Full GC - 备选):
-
触发条件 (应尽量避免):
-
晋升失败(年轻代 GC 时 Survivor 和 Old 空间都不足)。
-
混合回收速度跟不上应用分配新对象的速度(并发标记周期完成前堆就满了)。
-
大对象分配找不到足够的连续 Humongous Region。
-
元空间(Metaspace)不足。
-
-
过程: G1 会退化使用 Serial Old GC 算法(单线程的 Mark-Sweep-Compact),对整个堆进行标记-清除-压缩。停顿时间非常长!
-
目标: 通过调整参数(堆大小、IHOP、GC线程数等)和优化应用(减少内存分配、减少大对象、避免过早晋升)来完全避免 Full GC。
-
五、G1 的优势
-
可预测的低延迟: 通过 MaxGCPauseMillis 控制目标停顿时间,适合需要响应时间的应用(如 Web 服务、交易系统)。
-
高吞吐量: 在满足停顿时间目标的前提下,利用多核并行处理,保持较高的应用吞吐量。
-
大堆高效管理: Region 化设计和部分收集机制使其能有效管理超大堆内存。
-
空间整合: 基于复制的回收算法避免了内存碎片问题。
-
设计先进: 并发标记、SATB、精细的 RSet 等机制充分利用现代硬件。
六、G1 的劣势与调优考虑
-
内存占用 (Footprint):
-
RSet 需要额外内存开销(通常堆的 5%-20%)。
-
写屏障(维护 RSet 和 SATB)会带来一定的运行时开销(CPU)。
-
-
更复杂的调优:
-
-XX:MaxGCPauseMillis
:设置合理的目标(太激进会导致频繁 GC 降低吞吐量,太宽松则失去意义)。不要期望设置为 10ms 就能达到,需要根据硬件和应用实际情况调整。 -
-XX:InitiatingHeapOccupancyPercent
:触发并发标记周期的堆占用阈值。如果 Mixed GC 回收速度跟不上对象晋升速度导致过早 Full GC,可能需要降低 IHOP。如果并发标记启动过早(老年代垃圾不多),可以适当提高。 -
-XX:ConcGCThreads
/-XX:ParallelGCThreads
:控制并发和并行阶段的线程数。 -
-XX:G1NewSizePercent
/-XX:G1MaxNewSizePercent
:虽然动态调整,但仍可设置年轻代大小的上下限。 -
-XX:G1HeapRegionSize
:显式设置 Region 大小(通常是 2 的幂,1MB~32MB)。巨型对象大小会影响 Region Size 的选择。
-
-
巨型对象 (Humongous Objects):
-
分配和回收可能更复杂(需要连续 Region)。
-
频繁分配/释放大对象可能导致堆碎片(虽然 G1 能避免传统碎片,但对 Humongous 连续空间需求可能造成问题)或过早 Full GC。尽量优化应用避免过多大对象。
-
七、何时使用 G1?
-
应用需要较低的、可预测的停顿时间(如延迟敏感型服务)。
-
运行在大内存(>=6GB) 的服务器上。
-
Full GC 不可接受 或需要避免(通过 G1 的 Mixed GC 机制)。
-
应用有中等或较高的吞吐量要求(在满足延迟前提下)。
-
JDK 8 update 40 或更高版本(生产环境建议用较新稳定版 JDK 11/17/21)。
八、总结
G1 垃圾回收器是 JVM 垃圾回收技术的一个重要里程碑。它通过 Region 化堆、Remembered Set、Collection Set 和并发标记等创新设计,在提供可预测低停顿时间的同时,兼顾了高吞吐量,并有效解决了大堆管理和内存碎片问题。虽然调优相对复杂且有一定内存开销,但它已成为现代 Java 应用(特别是服务端应用)默认且推荐的垃圾回收器。