G1 GC 深度:Region、SATB 与 Mixed GC

24 阅读1小时+

概述

前文《垃圾收集器全景:Serial/Parallel/CMS/G1》揭示了 CMS 并发标记清除的三大结构性缺陷:碎片化导致的 Serial Old Full GC、增量更新并发标记的漏标风险、以及停顿不可预测的回收模型。G1 正是为解决这些缺陷而设计的替代方案。在深入其算法细节之前,有必要先给出 G1 的完整定义,建立贯穿全文的概念框架。 基于你的要求,我直接在概述中补充一段精确的 G1 与 CMS 技术对比总结,以“问题 → G1 技术 → 解决效果”的格式,清晰展示 G1 的结构性优势。你可以将此内容嵌入到概述中“G1 正是为解决这些缺陷而设计的替代方案”之后。

G1(Garbage-First)是一款面向服务器端应用、以可控低停顿为设计目标的并发分代垃圾收集器。它采用基于 Region 的分区堆布局,通过 SATB(Snapshot-At-The-Beginning)并发标记算法和 RSet(Remembered Set)跨区引用追踪机制,实现了以 Mixed GC 为标志的选择性增量式老年代回收。其核心设计原则是:在用户指定的停顿时间目标(-XX:MaxGCPauseMillis)约束下,优先回收垃圾占比最高、回收性价比最优的堆 Region,从而在吞吐量和延迟之间取得可量化的平衡。G1 首次在 JDK 7u4 以实验性特性引入,JDK 8u40 成熟为生产可用,JDK 9 起通过 JEP 248 成为 JVM 默认收集器。

G1 的设计哲学可凝练为三点,这三点贯穿本文所有算法模块:

分而治之(Divide and Conquer) :将连续堆空间打散为等大 Region(1MB~32MB,2 的幂),回收粒度从“整个分代”缩小为“一组 Region”。Region 的角色——Eden、Survivor、Old、Humongous——在运行期动态切换,G1 可自适应调整年轻代与老年代的逻辑边界,无需手动配置分代大小。

垃圾优先(Garbage-First) :Mixed GC 的 CSet 选择基于“回收价值”排序——单位停顿时间内可回收垃圾量越大的 Region,越被优先选中。该策略由并发标记产生的精确 Region 存活度统计和停顿预测模型共同支撑,确保每次 STW 的投入产出比最大化。

可预测停顿(Predictable Pause) :G1 的所有自适应机制——年轻代大小调整、IHOP 阈值推算、CSet 贪心选取、Mixed GC 次数分摊——均围绕 -XX:MaxGCPauseMillis 目标运作。它不保证每次停顿绝对低于目标,但通过闭环反馈控制使实际停顿分布在统计意义上可预测、可控制。

G1 对 CMS 缺陷的系统性解决,体现在以下五项技术组合:

CMS 缺陷缺陷本质G1 技术方案解决效果
碎片化标记-清除算法导致老年代空间碎片,碎片严重时触发单线程 Serial Old Full GCRegion 化堆布局 + 复制回收:以 Region 为回收粒度,存活对象复制到其他 Region,原 Region 整体归还空闲列表根除碎片化,避免因碎片导致的 Full GC
漏标风险增量更新依赖 post-write barrier 记录新值,无法从理论上杜绝并发标记期间的漏标SATB 快照并发标记:pre-write barrier 记录旧值,维护并发标记开始时的对象图快照,数学上保证不漏标杜绝漏标,代价是浮动垃圾稍多
停顿不可预测仅在老年代濒临耗尽时触发回收,回收动作集中且无法分摊为多次可控停顿Mixed GC + 停顿预测模型:并发标记结束后,基于 Region 存活度统计,用贪心算法选择回收价值最高的 Region 集合,累计预测时间不超过 MaxGCPauseMillis,并通过 G1MixedGCCountTarget 分摊为多次 STW老年代回收可预测、可分摊、可控制
跨代引用定位低效使用全局卡表,Minor GC 需扫描整个老年代脏卡,年轻代增大时效率下降RSet 三级粒度:每个 Region 维护独立的 RSet,精确记录“外部 Region 的哪些 Card 指向本 Region”,回收时仅扫描 RSet 记录的外部 Region,无需全堆扫描跨 Region 引用追踪精确化,扫描开销可控
大对象处理大对象直接分配在老年代,CMS 无法增量回收,Full GC 时移动代价高Humongous Region 专用区:大对象在连续 Region 组分配,不参与复制回收,仅在 Cleanup 阶段判定死亡后整体释放避免复制大对象的开销,但回收有延迟

这五项技术组合,使 G1 在延迟可控性上实现对 CMS 的代际超越,同时将吞吐量开销控制在可接受范围——这是 G1 取代 CMS 成为默认收集器的技术根基。

理解 G1,本质上是在理解它如何在延迟(Latency)吞吐量(Throughput) 之间寻找可控平衡点。SATB 的 pre-write barrier 和 RSet 的 post-write barrier 共同增加了应用线程的执行开销,这是 G1 吞吐量通常低于 Parallel 收集器的根源;但双重写屏障换来了绝对不漏标和无需全堆扫描的精确跨 Region 引用追踪,从而实现了 CMS 无法企及的停顿可预测性。这种“以吞吐换延迟可控”的权衡,是本文展开深度分析时贯穿始终的暗线。

本文将深入到 SATB 并发标记、RSet 跨 Region 引用维护、和 Mixed GC 停顿预测模型的核心算法中,以“一次 G1 Mixed GC 从并发标记到回收完成的完整流程”为主线,串联全部核心机制。

核心要点

  • Region 类型:Eden/Survivor/Old/Humongous,大小 1M-32M,2 的幂,TLAB 在 Eden Region 内分配
  • SATB 并发标记:pre-write barrier 记录旧值,保证不漏标,浮动垃圾增多,五阶段交替 STW 与并发
  • RSet 维护:post-write barrier + 异步 Refine 线程,三级粒度(Sparse→Fine→Coarsening)权衡内存与扫描开销
  • Mixed GC CSet:基于回收价值(垃圾量/预测回收时间)贪心选择,累计预测时间不超过 MaxGCPauseMillis
  • 停顿预测模型:基于历史存活对象数据与衰减平均,预测每个 Region 回收耗时,指导 CSet 选择
  • Young GC:多线程并行复制,动态年龄判断,自适应调整年轻代 Region 数量
  • Humongous Region:大对象直接分配在老年代连续 Region,仅在 Cleanup 阶段或 Full GC 回收

文章组织架构

flowchart TD
    A["第一章: 全景序章<br/>G1 完整生命周期流程"] --> B["第二章: G1 Region类型<br/>Eden/Survivor/Old/Humongous 与大小设置"]
    B --> C["第三章: SATB 并发标记<br/>pre-write barrier、SATB队列与五阶段"]
    C --> D["第四章: RSet 维护机制<br/>写屏障、三级粒度与 Coarsening 优化"]
    D --> E["第五章: Mixed GC CSet 选择<br/>回收价值排序、停顿预测模型与贪心选取"]
    E --> F["第六章: Young GC 流程<br/>多线程复制、年龄晋升与动态年轻代调整"]
    F --> G["第七章: Humongous Region<br/>大对象分配与 Cleanup 阶段回收"]
    G --> H["第八章: 参数调优与工程实战<br/>IHOP、G1MixedGCCountTarget、G1ReservePercent"]

    classDef ch1 fill:#d4e2f0,stroke:#3a6b92,stroke-width:1.5px,color:#1e3a5f
    classDef ch2 fill:#d0e8e0,stroke:#2c7a5e,stroke-width:1.5px,color:#1e4a3a
    classDef ch3 fill:#e0d8f0,stroke:#7a6aaa,stroke-width:1.5px,color:#3a2a6a
    classDef ch4 fill:#f2e6d8,stroke:#c0844a,stroke-width:1.5px,color:#6a3a1a
    classDef ch5 fill:#f5e0da,stroke:#b56a6a,stroke-width:1.5px,color:#6a2a2a
    classDef ch6 fill:#cce2ef,stroke:#4a6e8a,stroke-width:1.5px,color:#1e4a6a
    classDef ch7 fill:#e8e0d5,stroke:#a68a6c,stroke-width:1.5px,color:#4a3b2c
    classDef ch8 fill:#d6d8db,stroke:#5a6a7a,stroke-width:1.5px,color:#2a3a4a

    class A ch1
    class B ch2
    class C ch3
    class D ch4
    class E ch5
    class F ch6
    class G ch7
    class H ch8

分层说明:第一章建立 G1 全生命周期的宏观认知;第二章建立 Region 化堆的物理基础;第三、四章是全文核心——SATB 并发标记算法与 RSet 跨 Region 引用维护;第五章深入 Mixed GC 的 CSet 选择与停顿预测模型;第六、七章补充 Young GC 和 Humongous Region 的特殊处理;第八章回归参数调优与工程实践。关键结论:G1 通过 SATB 并发标记(pre-write barrier 记录旧值快照)解决了 CMS 增量更新的漏标问题,通过 RSet(三级粒度 + 写屏障维护)实现了精确的跨 Region 引用记录,通过停顿预测模型 + CSet 贪心选择实现了可预测的 Mixed GC 停顿。理解 SATB 和 RSet 是理解 G1 性能特征的关键——SATB 的写屏障比 CMS 更重,RSet 的内存占用和 Coarsening 是 G1 调优的核心考量。


第一章 全景序章:G1 垃圾收集完整生命周期流程

G1 的垃圾收集行为可抽象为一条闭环链路:从对象分配开始,经历分代晋升、并发标记、混合回收,直至空间归还。下示流程图覆盖了该链路中的所有关键决策节点与执行阶段,各节点均对应特定的数据结构和算法机制。理解此全景图是后续深入各模块的前提。

flowchart TD
    Start(["应用线程分配对象"]) --> CheckSize{"对象大小<br/>>= RegionSize/2 ?"}
    
    CheckSize -- "是" --> HumongousAlloc["直接分配到连续的<br/>Humongous Region"]
    CheckSize -- "否" --> EdenAlloc["分配到 Eden Region<br/>优先使用 TLAB"]
    
    EdenAlloc --> EdenFill{"Eden Region 空间不足?"}
    EdenFill -- "否" --> Return1(["快速分配完成"])
    EdenFill -- "是" --> YoungGCStart["触发 Young GC - STW<br/>多 GC 线程并行执行"]
    
    YoungGCStart --> YGCRoots["扫描 GC Roots 和<br/>CSet Region 的 RSet<br/>定位所有存活对象"]
    YGCRoots --> YGCCopy["存活对象复制到<br/>To-Survivor Region"]
    YGCCopy --> YGCAge["检查对象 GC 年龄"]
    YGCAge --> AgeCompare{"年龄 >= MaxTenuringThreshold<br/>或满足动态年龄判定?"}
    AgeCompare -- "是" --> PromoteToOld["晋升: 复制到 Old Region"]
    AgeCompare -- "否" --> KeepInSurvivor["留在 To-Survivor Region<br/>GC 年龄 +1"]
    
    PromoteToOld --> OldAccumulate["Old Region 存活对象积累<br/>老年代占用率上升"]
    KeepInSurvivor --> SurvivorSwap["From-Survivor 清空<br/>To-Survivor 变为新的 From"]
    
    YGCCopy --> ClearEden["清空所有 Eden Region<br/>归还空闲列表"]
    ClearEden --> Return2(["Young GC 结束"])
    
    HumongousAlloc --> HumongousCheck{"老年代有足够<br/>连续空闲 Region?"}
    HumongousCheck -- "否" --> FallbackFullGC["触发 Full GC<br/>全堆压缩整理"]
    HumongousCheck -- "是" --> HumongousAllocOK["分配成功:<br/>Humongous Starts +<br/>N-1 个 Continues Region"]
    HumongousAllocOK --> HumongousLife["巨型对象长期驻留<br/>不参与 Young GC 和 Mixed GC"]
    
    HumongousLife --> IHOPTrigger["巨型对象增加老年代占用"]
    OldAccumulate --> IHOPTrigger
    IHOPTrigger --> IHOPCheck{"整堆占用比例 >=<br/>InitiatingHeapOccupancyPercent?"}
    IHOPCheck -- "否" --> WaitMore["继续分配, 等待阈值触发"]
    WaitMore --> IHOPCheck
    IHOPCheck -- "是" --> ConcurMarkCycle["启动并发标记周期"]
    
    ConcurMarkCycle --> InitialMark["阶段1: 初始标记 - STW<br/>标记 GC Roots 直接可达<br/>通常与 Young GC 合并执行"]
    InitialMark --> RootRegionScan["阶段2: 根 Region 扫描 - 并发<br/>扫描 Survivor Region<br/>确保跨代引用被追踪"]
    RootRegionScan --> ConcurMark["阶段3: 并发标记 - 并发<br/>遍历整个对象图<br/>应用线程通过 pre-write barrier<br/>将旧值写入 SATB 队列"]
    ConcurMark --> Remark["阶段4: 最终标记 - STW<br/>清空所有 SATB 队列剩余<br/>处理引用变更"]
    Remark --> Cleanup["阶段5: 清理 - STW 部分<br/>计算所有 Region 存活度<br/>识别完全空闲 Region<br/>回收死 Humongous 对象"]
    
    Cleanup --> MixedGCPrep["准备 Mixed GC<br/>按回收价值排序<br/>选择 CSet 候选 Old Region"]
    MixedGCPrep --> MixedGCStart["Mixed GC - STW<br/>CSet = 所有 Young Region<br/>+ 选中的 Old Region"]
    
    MixedGCStart --> MixedGCYoung["回收所有 Eden 和<br/>From-Survivor Region"]
    MixedGCStart --> MixedGCOld["回收选中的 Old Region:<br/>存活对象复制到其他<br/>Old/Survivor Region"]
    MixedGCOld --> OldRegionFree["原 Old Region 清空<br/>归还空闲列表"]
    
    MixedGCYoung --> Return3(["空间回收完成"])
    OldRegionFree --> Return3
    
    HumongousLife --> MarkHumongous["并发标记也追踪<br/>Humongous 对象存活状态"]
    MarkHumongous --> CleanupPhase["Cleanup 阶段判定:<br/>死 Humongous 对象直接释放"]
    CleanupPhase --> FreeHumongous["连续 Region 组归还空闲列表"]
    FreeHumongous --> Return4(["Humongous 空间回收"])
    
    FallbackFullGC --> FullGCDetails["Full GC: 全堆压缩 - STW<br/>并行标记-整理<br/>所有 Region 角色重置"]
    FullGCDetails --> Return5(["堆空间强制整理"])

    classDef startEnd fill:#fef3c7,stroke:#d97706,color:#92400e;
    classDef decision fill:#ede9fe,stroke:#8b5cf6,color:#4c1d95;
    classDef process fill:#f1f5f9,stroke:#334155,color:#1e293b;

    class Start,Return1,Return2,Return3,Return4,Return5 startEnd;
    class CheckSize,EdenFill,AgeCompare,HumongousCheck,IHOPCheck decision;
    class HumongousAlloc,EdenAlloc,YoungGCStart,YGCRoots,YGCCopy,YGCAge,PromoteToOld,KeepInSurvivor,OldAccumulate,SurvivorSwap,ClearEden,FallbackFullGC,HumongousAllocOK,HumongousLife,IHOPTrigger,WaitMore,ConcurMarkCycle,InitialMark,RootRegionScan,ConcurMark,Remark,Cleanup,MixedGCPrep,MixedGCStart,MixedGCYoung,MixedGCOld,OldRegionFree,MarkHumongous,CleanupPhase,FreeHumongous,FullGCDetails process;

1.1 分配阶段:对象分流与 TLAB 机制

对象分配在 G1 中面临两条互斥路径,由对象大小Region 大小的比值决定。Region 大小由 -XX:G1HeapRegionSize 指定,取值范围 1MB~32MB,且必须为 2 的幂次。对象大小达到 Region 大小 50% 时,被分类为巨型对象(Humongous Object),否则为普通对象

普通对象分配进入 Eden Region。为降低多线程分配时的竞争,每个 Java 线程在 Eden Region 内部预先申请一块线程本地分配缓冲(Thread Local Allocation Buffer, TLAB)。线程在 TLAB 内部使用指针碰撞(Bump-the-Pointer)进行无锁分配;仅当 TLAB 耗尽时,才需向堆全局空闲列表申请新的 TLAB 空间。该机制将高频率的小对象分配开销从全局同步降至局部操作。TLAB 的大小可通过 -XX:TLABSize 指定,JVM 同时支持自适应调整(-XX:+ResizeTLAB),根据线程历史分配速率动态缩放。

巨型对象分配绕过整个年轻代,直接向老年代申请连续 Region 组。分配逻辑位于 g1CollectedHeap.cppG1CollectedHeap::humongous_obj_allocate() 方法:它首先计算所需 Region 数量 N,然后遍历空闲列表查找连续 N 个空闲 Region。若成功,将这些 Region 标记为 1 个 Humongous Starts 和 N-1 个 Humongous Continues,组成逻辑连续空间。巨型对象被直接视为老年代对象,不参与年轻代复制,避免复制大对象的高昂代价。此分配路径的副效应是:即使巨型对象在下一次 Young GC 之前就已死亡,其占用的空间也无法立刻回收,必须等到并发标记周期的 Cleanup 阶段。

分配失败与 Full GC 触发:当巨型对象分配无法在老年代找到足够连续 Region 时,G1 将触发Full GC 执行全堆压缩,重整空间碎片后再次尝试分配。这是 G1 设计中应竭力避免的严重停顿事件。此外,若 Young GC 期间晋升对象时老年代剩余空间(含 -XX:G1ReservePercent 预留空间)不足,也会触发 Full GC(晋升失败)。

1.2 Young GC:并行复制与分代晋升

当 Eden Region 空间不足以容纳新的对象分配请求时,G1 触发Young GC。这是一次 STW 暂停,所有应用线程被挂起,由 -XX:ParallelGCThreads 指定数量的 GC 工作线程并行执行回收。

根枚举与 RSet 扫描:GC 线程首先从一组 GC Roots(线程栈帧、JNI 句柄、类静态变量、同步监视器等)出发,扫描直接可达对象。随后,扫描 CSet 中每个 Region 的RSet(Remembered Set)。RSet 记录了从其他 Region 指向本 Region 内部对象的跨 Region 引用。借助 RSet,G1 仅需精确扫描那些“存在指向 CSet 的引用”的外部 Region 的 Card,而无须遍历整个堆。此过程对应于 GC 日志中的 “Scanning Remembered Sets” 阶段,其耗时与 RSet 的粒度和大小强相关。

对象复制与年龄增长:标记出的存活对象从 Eden Region 和 From-Survivor Region 向 To-Survivor Region 复制,此为标准的复制回收算法。每经历一次 Young GC 且存活的对象,其对象头中的 GC 年龄字段(_age)递增 1。该字段占 4 位,故最大年龄为 15。

晋升判定:晋升至 Old Region 的条件包括两条路径:

  • 固定年龄阈值:GC 年龄达到 -XX:MaxTenuringThreshold(默认 15)。
  • 动态年龄计算:若 Survivor 空间中某一年龄及以上的所有对象大小总和超过 Survivor 空间的 50%,则大于等于该年龄的对象直接晋升。该机制由 -XX:TargetSurvivorRatio(默认 50)控制,目的是避免 Survivor 空间被逐渐填满,引发不必要的过早晋升。

晋升对象复制到空闲 Old Region,该 Region 随之转变为 Old 类型。未晋升的对象保留在 To-Survivor Region,年龄加一。原 Eden 和 From-Survivor Region 被整体清空并归还给空闲列表,用于下一轮分配。

1.3 老年代积累与并发标记触发

随着 Young GC 不断晋升对象,以及巨型对象直接分配,Old Region 中的存活数据持续累积,老年代占用率上升。G1 的堆占用率指整堆(Eden + Survivor + Old + Humongous)已使用空间占总堆容量的比例。

IHOP 阈值检测:参数 -XX:InitiatingHeapOccupancyPercent(IHOP,默认 45)定义了触发并发标记周期的堆占用率门槛。当占用率首次达到该阈值时,G1 启动并发标记。在自适应模式下(-XX:+G1UseAdaptiveIHOP,JDK 9+ 默认开启),IHOP 值并非固定,而是基于近期的对象分配速率与历史标记耗时,动态推算一个“最晚安全启动点”,使得标记周期能在老年代完全用尽前完成,避免并发标记失败。自适应 IHOP 的算法位于 g1CollectorPolicy.cppG1AdaptiveIHOP 类中,其核心是维护一个“标记周期耗时 + 并发期间预期分配量”的预测模型。

1.4 并发标记周期:SATB 算法与五阶段协同

并发标记周期是整个 G1 回收周期的核心,负责在全堆范围内标定存活对象。它由五个阶段组成,交替使用 STW 与并发执行,将最繁重的对象图遍历工作交由并发线程完成,STW 仅用于同步边界。

阶段 1:初始标记(Initial Mark, STW)。标记从 GC Roots 直接可达的对象。此阶段通常与一次 Young GC 合并(piggybacked),以减少独立 STW 停顿次数。合并执行时,Young GC 的根扫描工作复用于并发标记的初始根集合。

阶段 2:根区域扫描(Root Region Scanning, 并发)。在初始标记之后的并发阶段,专门扫描 Survivor Region 中的跨代引用。由于 Young GC 可能在此阶段发生并晋升对象,根区域扫描确保所有从 Survivor 指向老年代的引用均被追踪,防止因并发晋升导致的漏标。被扫描的 Survivor Region 被称为“根 Region”,其集合在初始标记的 STW 期间被确定,此后的 Young GC 产生的新 Survivor Region 不会被加入,以保证并发扫描的终止性。

阶段 3:并发标记(Concurrent Marking, 并发)。多个并发标记线程(数量由 -XX:ConcGCThreads 控制)从初始标记和根区域扫描获得的根集合出发,递归遍历整个对象图。与应用线程并发执行期间,应用线程通过pre-write barrier 维护 SATB 快照一致性:在引用类型字段赋值前,将被覆盖的旧值推入线程本地 SATB 缓冲区;缓冲区满时转移至全局 SATB 队列。并发标记线程从全局队列中取出旧值,将其视为存活对象继续追踪,确保并发标记开始时对象图的快照完整性,逻辑上不漏标。

阶段 4:最终标记(Remark, STW)。停止所有应用线程,清空全局 SATB 队列中的所有剩余记录,处理并发标记阶段遗漏的引用变更,完成最终存活对象标记。此阶段还执行一些轻量级的引用处理(软引用、弱引用、虚引用、终结器)和 JNI 弱引用清理。

阶段 5:清理(Cleanup, STW + 并发)。计算每个 Region 的存活对象统计信息:通过对比本轮标记位图(nextBitmap)与上一轮标记位图(prevBitmap),得出每个 Region 的存活字节数和垃圾字节数。完全空闲的 Region 立即归还空闲列表;死巨型对象(无任何引用)的连续 Region 组也在此阶段回收。存活度统计信息作为后续 Mixed GC 的 CSet 选择输入。此外,该阶段还会根据存活度信息对 Old Region 按回收价值排序,生成 Mixed GC 候选集合。

1.5 Mixed GC:CSet 选择与停顿预测模型

并发标记完成后,G1 获得了所有 Old Region 的存活度信息。此时进入Mixed GC 阶段,回收任务由多次 STW 暂停共同完成,每次 Mixed GC 的 CSet 包含所有 Young Region 外加一部分选定的 Old Region。

CSet 选择算法执行以下步骤:

  1. 候选过滤:存活对象占比超过 -XX:G1MixedGCLiveThresholdPercent(默认 85%)的 Old Region 被直接排除——回收价值过低。
  2. 价值排序:对剩余候选 Region,计算回收价值 = 可回收垃圾量 / predict_region_elapsed_time_ms()。预测函数基于历史 GC 统计中的存活字节数与实际耗时,使用衰减平均模型估算回收该 Region 所需时间。
  3. 贪心选取:按回收价值降序排列,依次选入 CSet 并累加预测耗时,直至累计预测时间达到 -XX:MaxGCPauseMillis 目标,或所有候选被选完。

分摊回收机制:候选 Old Region 的回收由 -XX:G1MixedGCCountTarget(默认 8)次 Mixed GC 分摊执行。若候选 Region 过多,G1 会将单次 Mixed GC 未能选完的 Region 留待后续 Mixed GC,避免单次停顿超标。

执行过程:每次 Mixed GC 的 CSet = 所有 Young Region + 选中的 Old Region。Young Region 使用复制回收(与 Young GC 相同),Old Region 中的存活对象复制到其他空闲 Old Region 或 Survivor Region,原 Region 清空归还。整个流程多线程并行完成。

1.6 巨型对象回收与 Full GC 兜底

巨型对象不参与 Mixed GC 的复制回收,其生命周期终结依赖两个时机:

  1. Cleanup 阶段:并发标记周期末尾的 Cleanup 阶段,若判定某巨型对象已无任何引用,其占用的连续 Region 组直接整体归还空闲列表。此为常规回收路径。
  2. Full GC:若巨型对象在多次标记周期后仍存活,而堆空间因碎片化或晋升压力耗尽,触发 Full GC。此时全堆使用标记-整理算法压缩,清除所有死巨型对象并重整空间。JDK 10+ 的 G1 Full GC 已改为并行执行(JEP 307),显著缩短了最坏停顿时间。

Full GC 触发条件不限于巨型对象分配失败,还包括晋升失败(Promotion Failure)、并发标记失败(Concurrent Mark Failure)、System.gc() 显式调用、以及元空间分配失败导致的全堆收集。

1.7 闭环总结

上述流程构成 G1 的完整生命周期闭环:分配—晋升—标记—选择—回收。SATB 并发标记提供 Region 级存活度数据,停顿预测模型基于这些数据做出回收决策,Mixed GC 执行分代混合回收,三者的精确协同使 G1 得以在给定的停顿预算约束下,优先回收垃圾占比最高、回收效率最优的 Region。流程图中的每一个分支节点,均可映射到具体的 JVM 参数和 HotSpot 源码模块,后续各章将逐一对这些核心机制展开深度分析。


第二章 G1 Region 类型:Eden/Survivor/Old/Humongous 与大小设置

Region 是 G1 堆管理的最小单元,也是实现细粒度回收和可预测停顿的物理基础。理解 Region 的类型系统、动态角色转换和大小选择策略,是分析 G1 行为的第一步。

2.1 Region 基本属性与类型

G1 堆被划分为大小相等的 Region,大小由 -XX:G1HeapRegionSize 指定,取值范围 1MB 到 32MB,且必须为 2 的幂。若未显式指定,JVM 将根据最大堆大小(-Xmx)计算默认 Region 大小,目标是使 Region 数量接近 2048 个。计算公式近似为:RegionSize = MAX(1MB, MIN(32MB, heap_size / 2048)),并向上取整为最接近的 2 的幂。

每个 Region 在 HotSpot 源码中由 heapRegion.hppHeapRegion 类表示。其类型通过 _type 字段标识,可能的取值为:

  • Eden:年轻代分配区域。新对象(除巨型对象外)在此分配,TLAB 也在此区域内。
  • Survivor:年轻代存活对象暂存区。Young GC 后存放存活对象,有 From/To 之分。
  • Old:老年代区域。存放从 Survivor 晋升或直接分配的长期存活对象。
  • Humongous:巨型对象区。用于存放大小超过单个 Region 50% 的对象,由一组连续 Region 构成,分为 Starts 和 Continues。

Region 的角色是动态的:一个 Region 在生命周期内可以在空闲、Eden、Survivor、Old 之间多次转换。当 Eden Region 中的对象在 GC 后全部被复制走或死亡,该 Region 会被清空并回到空闲列表,随后可能作为 Survivor、Eden 或 Old 重新分配。这种动态角色模型使得 G1 免于传统的固定分代内存界限,回收粒度更精细。

flowchart LR
    subgraph G1堆 [G1 Heap: 划分为等大Region]
        direction TB
        E(Eden Region 1<br/>TLAB 分配中)
        F(Eden Region 2<br/>已满)
        G(Survivor Region<br/>From)
        H(Survivor Region<br/>To)
        I(Old Region 1<br/>存活率高)
        J(Old Region 2<br/>垃圾多)
        K(Humongous Region<br/>Starts)
        L(Humongous Region<br/>Continues)
        M(空闲 Region)
        
        E -->|Young GC 后| F
        F -->|对象晋升| I
        G -->|复制存活对象到| H
        I -->|Mixed GC 被选入CSet| J
        K -->|连续Region组| L
    end

a) 主旨概括:该图展示了 G1 堆中 Region 的动态类型分布,从 Eden、Survivor、Old 到 Humongous,以及对象在不同类型 Region 之间的流转路径。

b) 逐元素分解

  • Eden Region 1:包含活跃 TLAB 的 Eden Region,正在接受新对象分配。TLAB 区域在 Region 内部通过指针碰撞线性分配。
  • Eden Region 2:已满的 Eden Region,等待 Young GC 回收。
  • Survivor (From/To):Young GC 的源和目的区域,存活对象在两次 Survivor 间复制,年龄递增。
  • Old Region 1/2:老年代区域,分别代表高存活率和低存活率场景,后者是 Mixed GC 的目标。
  • Humongous Starts/Continues:巨型对象独占的连续 Region 组。
  • 空闲 Region:可被重新分配为任何类型的空白区域。

c) 设计原理映射:Region 动态角色模型是 G1 “化整为零” 思想的核心。通过将堆打散为等大 Region,并允许角色动态转换,G1 得以在回收时仅关注垃圾集中的部分 Region,避免 CMS 式全堆扫描和 Serial/Parallel Old 式全堆整理的高昂代价。

d) 工程联系与关键结论Region 大小的选择直接决定 G1 的回收粒度与控制效率。Region 过小会导致 Region 总数膨胀,RSet 内存占用增加,巨型对象判定门槛降低(更多对象被当作 Humongous 处理);Region 过大会导致内部碎片增加,Mixed GC 单次回收粒度变粗,难以精细控制停顿。生产实践中,推荐令 Region 数量接近 2048 的默认推导值,以获得粒度与开销的最佳平衡。

2.2 TLAB 在 Region 内的分配

TLAB 机制在 G1 的落地方式为:每个线程在 Eden Region 内部申请一块固定大小的线程私有空间,对象分配在该空间内通过指针碰撞(top 指针移动)完成。当 TLAB 内剩余空间不足以容纳当前对象时,线程向堆申请新的 TLAB;若对象大小超过 TLAB 可容纳上限,则直接在 Eden Region 的共享空间(而非 TLAB)中分配,称为“慢速分配”。

TLAB 的大小由 -XX:TLABSize 指定,或由 JVM 自适应调整。G1 的 TLAB 分配路径位于 g1CollectedHeap.cppG1CollectedHeap::attempt_allocation() 方法,该方法首先尝试在 TLAB 中分配,失败后回退到 Region 内的共享分配路径。

2.3 Humongous Region 的特殊性

当对象大小达到 G1HeapRegionSize / 2 时,G1 将其视作巨型对象。分配路径绕过年轻代,直接通过 G1CollectedHeap::humongous_obj_allocate() 在老年代查找连续空闲 Region 组。分配成功后,起始 Region 被标记为 Humongous Starts,后续 Region 被标记为 Humongous Continues,对象横跨整个连续区域。

巨型对象一旦分配,不参与任何形式的复制回收(Young GC 或 Mixed GC 的复制阶段均不移动它),其空间回收仅发生在两个时机:并发标记周期结束的 Cleanup 阶段(死巨型对象被整体释放),或 Full GC 的全堆压缩。因此,频繁分配短命巨型对象是 G1 的反模式,会导致老年代被快速填充,严重时引发 Full GC。


第三章 SATB 并发标记:pre-write barrier、SATB 队列与五阶段

SATB(Snapshot-At-The-Beginning)并发标记算法是 G1 解决 CMS 增量更新漏标问题的核心设计。它通过记录并发标记开始时对象图的逻辑快照,并借助 pre-write barrier 维持快照一致性,从数学上杜绝了漏标可能性,代价是产生更多浮动垃圾。

3.1 设计目标与核心原理

并发标记期间,应用线程在修改对象引用关系的同时,GC 线程在遍历对象图。经典的“三色标记”抽象中,可能出现两种漏标情况:

  • 黑色对象新增对白色对象的引用:若标记线程已处理完某个黑色对象(其所有字段均已扫描),此时应用线程在该黑色对象上新增一个指向白色对象的引用,该白色对象将被遗漏。
  • 灰色对象失去对白色对象的引用:标记线程正在处理灰色对象时,应用线程删除了其指向白色对象的引用,且该白色对象无其他灰色对象引用,也会漏标。

CMS 使用增量更新(Incremental Update)策略应对第一种情况——通过 post-write barrier 记录黑色对象的新引用目标,在 Remark 阶段重新扫描。但第二种情况仍可能发生,且 CMS 需通过多重重复标记来降低风险,不能完全避免。

SATB 采取的思路与此截然不同:将并发标记开始时的对象图视为不可变的“快照”。任何在快照之后发生的引用变化,均通过 pre-write barrier 记录被删除的旧引用(旧值),从而“复活”快照时刻存活的对象。这样,无论引用如何变更,快照时刻的可达对象均会被标记,逻辑上绝不漏标。快照之后新产生的对象和引用变化(导致的对象变成垃圾)不会被本次 GC 回收,即浮动垃圾。

3.2 pre-write barrier 机制

SATB 的关键是实现 pre-write barrier。在 Java 字节码层面,每个引用类型的字段赋值(putfieldputstaticaastore)之前,JIT 编译器会插入屏障代码。其逻辑等价于以下伪代码:

// 位于 satbQueue.hpp / satbQueue.cpp
void G1SATBCardTableModRefBS::write_ref_field_pre_static(void* field, oop new_val) {
  // 仅在并发标记活跃时执行
  if (concurrent_mark_is_active()) {
    oop old_val = *(oop*)field;           // 读取即将被覆盖的旧值
    if (old_val != NULL) {
      // 将旧值推入当前线程的 SATB 本地缓冲区
      satb_mark_queue().enqueue(old_val);
    }
  }
}

设计意图与生产影响:pre-write barrier 带来的开销有三部分:读取旧值的内存访问、条件判断(并发标记是否活跃)、以及入队操作(缓冲区满时需处理)。由于该屏障在每次引用写操作时执行,其性能开销是 G1 吞吐量低于 Parallel 收集器的重要原因。JIT 编译器会尝试优化——若逃逸分析证明对象不逃逸线程,则可消除其上的屏障。

与 CMS post-write barrier 的对比:CMS 的 post-write barrier 在赋值之后执行,记录的是“新值”或“被修改的卡”,目的是在 Remark 阶段找到新增引用。它不关心旧值。SATB 的 pre-write barrier 正好相反,只关心旧值。从并发正确性角度看,SATB 更健壮;从开销角度看,SATB 的 barrier 要做旧值读取,通常比 CMS 的卡标记更重。

flowchart TD
    subgraph 应用线程
        A[obj.field = new_val] --> B[pre-write barrier:<br/>读取旧值 old_val]
        B --> C{并发标记活跃?}
        C -- 是 --> D[将 old_val 推入<br/>本地 SATB 缓冲区]
        C -- 否 --> E[跳过]
        D --> F[执行写操作]
        E --> F
    end
    
    subgraph SATB队列处理
        G{本地缓冲区满?}
        G -- 否 --> H[完成]
        G -- 是 --> I[移交全局 SATB 队列<br/>分配新本地缓冲区]
        I --> J[并发标记线程<br/>从全局队列取 old_val]
        J --> K[将 old_val 作为 GC Root<br/>递归标记其引用]
    end
    
    D --> G

a) 主旨概括:此图展示了一次引用写操作时 pre-write barrier 的完整执行路径,包括旧值读取、条件判断、本地缓冲入队、全局队列移交及并发标记线程追踪的全过程。

b) 逐元素分解

  • 应用线程写操作:字段赋值前触发屏障,读取旧值;仅当并发标记活跃且旧值非空时才入队。
  • 本地 SATB 缓冲区:线程私有,无锁操作,快速路径(fast path)上屏障开销极低。
  • 全局队列移交:本地缓冲区满时触发,涉及全局锁和内存分配,是慢速路径(slow path)。
  • 并发标记线程追踪:从全局队列取到旧值后,将其视作 GC Root 递归标记,维持快照对象图完整性。

c) 设计原理映射:SATB 的 pre-write barrier 通过记录“即将消失的引用”,将并发标记的理论基础从“追踪当前存活对象”转变为“追踪快照时刻存活对象”。这种策略将并发标记的正确性证明简化为“快照原子性”问题,避免了 CMS 增量更新中复杂的引用变化追踪。

d) 工程联系与关键结论SATB 的 pre-write barrier 是 G1 吞吐量开销的主要来源之一。生产环境中,若发现应用线程停顿不是由于 GC STW,而是由于 SATB 缓冲处理过慢导致的强制 STW(日志中 “Pause Init Mark” 或 “Pause Remark” 耗时异常),可能需要增加 -XX:ConcGCThreads 并发标记线程数以加快队列消费,或降低 -XX:InitiatingHeapOccupancyPercent 提前开始标记,为队列处理留出更多时间窗口。

3.3 SATB 队列与并发处理

每个 Java 线程拥有一个固定大小的 SATB 本地缓冲区(默认约 1KB,可容纳 128 个 oop 指针)。当本地缓冲区满时,线程调用 SATBMarkQueue::flush() 将其移交至全局 SharedSATBQueue,并获取一个新的空缓冲区。全局队列的消费端是并发标记线程。并发标记线程循环从全局队列中取出旧值记录,对其指向的对象执行递归标记。若全局队列的消费速度持续低于生产速度,队列将不断增长。当全局队列超出阈值时,G1 会在最终标记阶段(Remark, STW)强制清空所有剩余记录,极端情况下甚至可能在并发标记期间插入一次 STW 辅助处理。

源码层面,SATB 队列实现在 satbQueue.hppsatbQueue.cppSATBMarkQueueSet 管理一组线程本地队列,enqueue_completed_buffer 方法将满缓冲区转移到全局列表,apply_closure_to_completed_buffer 被并发标记线程用来处理队列元素。

3.4 并发标记五阶段详细流程

并发标记周期由五个阶段组成,交替使用 STW 与并发执行。

阶段 1:初始标记(Initial Mark, STW)。标记从 GC Roots 直接可达的对象。此阶段通常与一次 Young GC 合并执行(piggybacking),以复用 Young GC 的根扫描结果,减少独立的 STW 停顿。日志中该阶段与 Young GC 一同标注为 “GC pause (G1 Evacuation Pause) (young) (initial-mark)”。

阶段 2:根区域扫描(Root Region Scanning, 并发)。扫描初始标记结束时确定的 Survivor Region(称为根 Region)中的跨代引用。因为并发标记期间 Young GC 可能发生并晋升对象到老年代,若不扫描 Survivor Region,晋升过程中建立的老年代引用可能漏标。根区域扫描必须在下次 Young GC 发生前完成对当前 Survivor Region 的扫描,因此 G1 会控制 Young GC 的频率,确保扫描进度领先于晋升速度。

阶段 3:并发标记(Concurrent Marking, 并发)。多个 -XX:ConcGCThreads 线程递归遍历对象图,使用 SATB 队列处理并发修改。标记结果记录在 nextBitmap 中。此阶段是并发标记周期中耗时最长的阶段,CPU 占用较高。

阶段 4:最终标记(Remark, STW)。停止所有应用线程,清空所有 SATB 队列剩余记录,处理引用变更,完成存活对象标记。同时处理 JNI 弱引用、Reference 对象队列等。此阶段是 STW 停顿中较显著的一次,其耗时受 SATB 队列积压程度影响。

阶段 5:清理(Cleanup, STW + 并发)。对比 nextBitmapprevBitmap,计算每个 Region 的存活字节和垃圾字节,生成 Mixed GC 候选 Region 列表。完全空闲的 Region 被立即回收;死巨型对象也被释放。此外,还会执行 RSet 的清理和字符串去重表的修剪(若开启 -XX:+UseStringDeduplication)。

flowchart TD
    A[并发标记周期启动] --> B[阶段1: 初始标记<br/>Initial Mark<br/>STW, 常与 Young GC 合并]
    B --> C[阶段2: 根区域扫描<br/>Root Region Scanning<br/>并发, 扫描 Survivor Regions]
    C --> D[阶段3: 并发标记<br/>Concurrent Marking<br/>并发, 遍历对象图<br/>SATB 队列协助]
    D --> E[阶段4: 最终标记<br/>Remark<br/>STW, 清空 SATB 队列<br/>处理引用变更]
    E --> F[阶段5: 清理<br/>Cleanup<br/>STW+并发, 计算存活度<br/>回收空闲和死 Humongous Region]
    F --> G[生成 Mixed GC 候选]

a) 主旨概括:该图展示 G1 并发标记周期的五个阶段及其执行模式(STW/并发),以及各阶段的职责和数据流转。

b) 逐元素分解

  • 初始标记:依赖 Young GC 的 STW,标记根集合。
  • 根区域扫描:保证并发标记期间晋升对象不丢失引用链。
  • 并发标记:核心并发阶段,最耗时,应用线程与标记线程赛跑。
  • 最终标记:处理残余,完成标记,STW 耗时取决于 SATB 队列积压。
  • 清理:账本核算,产出 Mixed GC 候选列表,释放明显空间。

c) 设计原理映射:五阶段设计将最繁重的对象图遍历完全并发化,仅在关键同步点(初始标记、最终标记、清理)进行短时 STW。根区域扫描是为 SATB 快照模型在分代晋升场景下设计的“补丁”,确保晋升对象被快照覆盖。

d) 工程联系与关键结论Remark 阶段的 STW 时间是调优的关键指标。若该阶段耗时过长,说明 SATB 队列积压严重,并发标记线程处理不过来。可尝试增加 -XX:ConcGCThreads(通常设为 ParallelGCThreads 的 1/4),或降低 IHOP 使标记更早开始,留足处理时间。日志中 “GC pause (G1 Remark) (final marking)” 的耗时需重点关注。

3.5 SATB 与 CMS 增量更新的深度对比

对比维度SATB (G1)增量更新 (CMS)
理论模型基于起点的对象图快照基于当前状态的对象图
写屏障类型pre-write barrier(记录旧值)post-write barrier(记录新值/卡标记)
漏标风险理论上绝对不漏标存在漏标风险,需多重 Remark 兜底
浮动垃圾多(快照后变为垃圾的对象本次不回收)少(仅无法避免的少量浮动垃圾)
写屏障开销较重(需读取旧值并入队)较轻(仅卡标记)
并发标记周期五阶段,根区域扫描特殊处理晋升四阶段,预清理和最终标记多次

第四章 RSet 维护:写屏障、三级粒度与 Coarsening 优化

RSet(Remembered Set)是 G1 实现高效跨 Region 引用管理的核心数据结构。它使 G1 在回收任意 CSet 时,无需全堆扫描即可定位所有指向 CSet 的引用入口。

4.1 RSet 的设计目标与原理

传统分代收集器维护一个全局卡表(Card Table),标记老年代中哪些卡页包含了指向年轻代的引用。Minor GC 时,只需扫描这些脏卡即可找到跨代引用。G1 的 Region 化堆打破了统一的老年代界限,跨 Region 引用可能发生在任意两个 Region 之间。若仍使用全局卡表,回收某个 Region 时需扫描整个堆的卡表,效率极低。

RSet 的解决方案是:每个 Region 维护自己的 RSet,记录“哪些外部 Region 的哪些 Card 引用了本 Region”。回收 Region R 时,只需扫描 R 的 RSet 中记录的外部 Region 和 Card,即可找到所有指向 R 的引用根,无需扫描其他 Region。

RSet 的维护通过 post-write barrier异步 Refine 线程 协同完成:

  1. 当发生跨 Region 的引用赋值时,post-write barrier 将引用源所在的 Card 标记为脏(卡表标记)。
  2. 专门的 Refine 线程异步扫描这些脏卡,解析出精确的跨 Region 引用关系,并更新到目标 Region 的 RSet 中。

这种“写屏障标记 + 异步更新”的策略将 RSet 维护从应用线程的关键路径上剥离,避免同步锁竞争和应用线程停顿。Refine 线程的数量和活跃程度由 JVM 自适应调整。

4.2 写屏障流程

G1 的 post-write barrier 逻辑比 CMS 更重,因为它不仅要更新卡表,还要触发 RSet 更新。简化后的逻辑如下:

// 伪代码:G1 post-write barrier
void post_write_barrier(oop* field, oop new_val) {
  // 1. 卡表标记(与 CMS 相同)
  dirty_card(calculate_card_addr(field));
  
  // 2. 若引用跨 Region,触发 RSet 更新
  if (is_cross_region_ref(field, new_val)) {
    // 将脏卡信息加入 Refine 线程的处理队列
    dirty_card_queue().enqueue(calculate_card_addr(field));
  }
}

dirty_card_queue 是一个全局队列,Refine 线程从中取出脏卡,解析该卡页内的对象,找出所有跨 Region 引用,并更新目标 Region 的 RSet。

4.3 RSet 的三级粒度与升级策略

为在内存占用和扫描效率之间取得平衡,RSet 内部采用自适应三级粒度结构:

Sparse(稀疏表):初始状态。使用哈希表直接存储外部 Region 的 Card 索引。每个条目记录 (外部Region索引, Card索引) 对。当跨 Region 引用稀少时,哈希表内存开销最小,但查找需进行哈希计算和链表遍历。

Fine(细粒度位图):当 Sparse 表达到阈值(由 -XX:G1RSetRegionEntries 控制,默认 1024)时,升级为 Fine 粒度。此时为每个引用了当前 Region 的外部 Region 维护一个 Card 位图。位图的每一位代表外部 Region 中的一个 Card(512 字节)。查找某个外部 Region 的引用时,直接按位索引,速度快于哈希表。内存开销为:N × (外部Region的Card数 / 8) 字节,N 为外部 Region 数。

Coarsening(粗粒度位图):当 Fine 位图的总内存占用超过阈值时,RSet 退化至 Coarsening 粒度。它放弃按外部 Region 分类的细分信息,用一个全局位图覆盖所有外部 Region。位图的每一位表示一个外部 Region 是否引用了当前 Region。扫描时,对于位图中标记的每个外部 Region,必须遍历该 Region 的全部 Card 来查找具体引用,导致扫描开销剧增。

升级/退化流程由 g1RemSet.cpp 中的 G1RemSet::add_reference() 和相关方法控制。当 RSet 检测到某外部 Region 的引用过于密集(覆盖了大量 Card),会将其从 Sparse 升级到 Fine;当内存压力大时,则主动触发 Coarsening,牺牲扫描效率以控制内存。

flowchart LR
    subgraph Region_R [Region R 的 RSet 结构]
        A[Sparse 初始状态<br/>哈希表: ExtReg, CardIdx]
        B[Fine 升级<br/>为每个外部Region建立Card位图]
        C[Coarsening 退化<br/>全局Region位图<br/>存在即扫描整个外部Region]
    end
    A -- 表项数量 > 阈值 --> B
    B -- 内存占用 > 阈值 --> C
    C -.->|Cleanup阶段重置| A

a) 主旨概括:此图描绘 RSet 从 Sparse 到 Fine 再到 Coarsening 的自适应升级路径,以及 Cleanup 阶段重置的机制。

b) 逐元素分解

  • Sparse:轻量级哈希表,适用于引用稀疏场景。
  • Fine:按外部 Region 组织的 Card 位图,查找快速,内存适中。
  • Coarsening:退化至全局 Region 位图,内存最低,扫描开销最大。
  • 升级/退化条件:表项数和内存占用超过阈值,阈值由 JVM 参数和内部自适应性控制。

c) 设计原理映射:三级粒度是空间-时间权衡(Space-Time Tradeoff)的经典工程实践。稀疏数据用精确结构节省空间,密集数据用索引结构加速访问,极端情况下牺牲精度换取内存安全。这种自适应机制使 G1 能应对不同负载下的引用模式。

d) 工程联系与关键结论当 Mixed GC 中 “Scanning Remembered Sets” 阶段耗时异常时,通常是 RSet Coarsening 导致。需重点排查哪些 Region 的 RSet 发生了退化。若无法从代码层面优化引用关系,可尝试增大 -XX:G1HeapRegionSize(减少 Region 总数,降低单个 Region 被引用的复杂度)或增大 -XX:G1RSetRegionEntries(延后 Fine 升级),但这些参数需谨慎调校,避免内存膨胀。


第五章 Mixed GC CSet 选择:回收价值排序、停顿预测模型与贪心选取

Mixed GC 是 G1 回收老年代的核心机制,它并非一次回收所有 Old Region,而是基于停顿预算进行选择性回收。其 CSet 选择算法融合了停顿预测模型和贪心策略,是“Garbage First”的直接体现。

5.1 触发条件与候选区域

并发标记周期结束后,Cleanup 阶段计算每个 Old Region 的存活度。存活对象占比低于 -XX:G1MixedGCLiveThresholdPercent(默认 85%)的 Region 进入候选回收集(Candidates Set)。存活度过高的 Region 回收价值极低,直接忽略。此外,巨型对象虽然也占用 Old Region,但不进入候选集——它们不参与 Mixed GC 的复制回收,仅在 Cleanup 阶段或 Full GC 时原地回收。

5.2 停顿预测模型

停顿预测模型是 G1 维持可预测停顿的数学基础。其核心函数为 G1Analytics::predict_region_elapsed_time_ms(HeapRegion* hr, bool for_young_gc)。该函数基于以下历史统计数据,使用衰减平均(decaying average)方法预测回收给定 Region 所需时间:

  • _cost_per_byte_ms:每复制一个字节存活对象所需的平均时间。当 Region 存活对象多时,复制开销大。
  • RSet 扫描开销:基于 Region 的 RSet 大小(记录的外部 Region 数)预测扫描 RSet 所需时间。
  • 其他固定开销:如对象头处理、引用更新等。

衰减平均的公式可简化为:new_avg = α × last_sample + (1 - α) × old_avg,其中 α 控制近期数据的权重(-XX:G1AverageDecayFactor,默认 0.98)。越近的 GC 数据对预测结果影响越大,使模型能快速适应负载变化。

5.3 CSet 选择算法

CSet 选择由 g1CollectorPolicy.cppG1Policy::finalize_cset() 执行,其贪心算法步骤如下:

  1. 初始 CSet:包含所有 Young Region(Eden 和 From-Survivor)。Young Region 的回收是必须的,且其预测时间已从年轻代大小自适应中体现。
  2. 候选排序:将所有候选 Old Region 按回收价值降序排列。回收价值公式为:
    Value = (Region_Total_Bytes - Live_Bytes) / predict_region_elapsed_time(hr)
    
    分子为可回收垃圾量,分母为预测回收耗时。该值表征“单位停顿时间可回收的垃圾量”,值越大,回收性价比越高。
  3. 贪心选取:从价值最高的 Region 开始,逐个累加预测耗时,直至总预测时间达到 -XX:MaxGCPauseMillis 目标,或候选集遍历完毕。
  4. 分摊控制:若一轮 Mixed GC 无法选完所有候选 Region,剩余 Region 将在后续 Mixed GC 中继续参与选择。总 Mixed GC 次数由 -XX:G1MixedGCCountTarget 控制,G1 会根据候选 Region 总数和该参数动态分摊每轮的回收量。
flowchart TD
    A[Cleanup 阶段生成候选 Old Region 列表] --> B[过滤: 存活率 < G1MixedGCLiveThresholdPercent]
    B --> C[计算每个候选 Region 的回收价值<br/>Value = 垃圾量 / 预测回收时间]
    C --> D[按 Value 降序排序]
    D --> E[初始化 CSet = 所有 Young Region<br/>累计时间 = Young Region 预测时间]
    E --> F{遍历候选列表}
    F -- 有剩余 --> G[取出 Value 最高的 Region]
    G --> H{累计时间 + Region 预测时间 <= MaxGCPauseMillis?}
    H -- 是 --> I[加入 CSet, 更新累计时间]
    I --> F
    H -- 否 --> J[丢弃该 Region 及之后所有]
    J --> K[完成 CSet 选择]
    K --> L[执行 Mixed GC: 回收 CSet]

a) 主旨概括:该流程图详述 Mixed GC 的 CSet 选择算法:从候选 Region 过滤开始,到价值排序、贪心选取、停顿预算约束,最后执行回收。

b) 逐元素分解

  • 存活率过滤G1MixedGCLiveThresholdPercent 作为第一道门槛,排除回收价值极低的 Region。
  • 回收价值公式:量化“Garbage First”,使算法聚焦于投入产出比最高的 Region。
  • 贪心选取:在停顿预算(MaxGCPauseMillis)约束下,选取价值最高的一批 Region,实现最优近似。
  • 余留处理:未选中的候选 Region 留待后续 Mixed GC,避免单次停顿超标。

c) 设计原理映射:该算法是将混合整数规划问题简化为贪心选择。因为预测模型提供的是每个 Region 的独立回收成本估计,贪心选取在大多数情况下能给出接近最优解的方案。停顿预测模型的准确性是算法有效的前提。

d) 工程联系与关键结论-XX:G1MixedGCCountTarget-XX:G1MixedGCLiveThresholdPercent 是调控 Mixed GC 停顿的核心参数。若 Mixed GC 频繁超时,可增大 G1MixedGCCountTarget 以分摊回收压力,或降低 G1MixedGCLiveThresholdPercent 以只回收高价值 Region。若老年代回收速度跟不上分配速度,导致堆积,则需反向调整。


第六章 Young GC 流程:多线程复制、年龄晋升与动态年轻代调整

G1 的 Young GC 虽然在设计上与传统分代收集器的 Minor GC 类似,但因 Region 化布局和 RSet 机制,其执行流程有诸多独特之处。

6.1 触发条件

当 Eden Region 空间不足时,触发 Young GC。G1 的年轻代由一组 Eden Region 和 Survivor Region 构成,其总大小受自适应算法控制,在 -XX:G1NewSizePercent(默认 5)和 -XX:G1MaxNewSizePercent(默认 60)之间动态调整。

6.2 多线程并行复制

Young GC 是一次完全的 STW 事件,所有应用线程被挂起。由 -XX:ParallelGCThreads 数量的 GC 工作线程并行执行以下步骤:

  1. 根扫描:扫描 GC Roots(线程栈、JNI 句柄、类静态变量、同步监视器等),找到直接可达对象。
  2. RSet 扫描:扫描 CSet 中每个 Region 的 RSet,获取所有从外部 Region 指向 CSet 的引用入口。这些入口也是根的一部分。
  3. 存活对象标记与复制:从根出发,递归标记所有存活对象,并将其复制到 To-Survivor Region 或 Old Region。复制过程采用工作窃取(work stealing)模型,GC 线程从共享任务队列获取复制任务,高效并行。
  4. 引用更新:所有存活对象复制完成后,更新指向这些对象的引用(使用“转发表”或直接修改)。
  5. 清理:清空 CSet 中的所有 Region(Eden 和 From-Survivor),归还空闲列表。

6.3 年龄晋升与动态年龄判定

对象每经历一次 Young GC 并存活,其对象头中的 GC 年龄字段(_age)递增。晋升至 Old Region 的条件为:

  • 固定阈值晋升_age >= MaxTenuringThreshold(默认 15)。
  • 动态年龄晋升:若 Survivor 空间中,某一年龄及以上的所有对象大小总和超过 Survivor 空间的 -XX:TargetSurvivorRatio(默认 50)比例,则大于等于该年龄的对象全部晋升。此机制由 ageTable 数据结构和 compute_tenuring_threshold 方法实现,目的是避免 Survivor 空间堆积大量中龄对象,导致过早填满。
flowchart LR
    subgraph Young GC前
        direction TB
        E1(Eden Region 1)
        E2(Eden Region ...)
        S1(From-Survivor Region)
        S2(To-Survivor Region 空)
        O1(Old Region)
    end
    
    subgraph Young GC中 [并行GC线程]
        GC1(GC 线程 1)
        GC2(GC 线程 2)
        GC3(GC 线程 ...)
    end
    
    subgraph Young GC后
        direction TB
        E3(空 Region)
        E4(空 Region)
        S3(From-Survivor Region 空)
        S4(To-Survivor Region 新)
        O2(Old Region 新晋升)
    end
    
    E1 & E2 & S1 -->|"扫描存活对象"| GC1 & GC2 & GC3
    GC1 & GC2 & GC3 -->|"复制存活对象、年龄+1"| S4
    GC1 & GC2 & GC3 -->|"晋升年龄达标对象"| O2
    GC1 & GC2 & GC3 -.->|"清空后"| E3 & E4 & S3

a) 主旨概括:此图展示 Young GC 的多线程复制过程,存活对象从 Eden/From-Survivor 流向 To-Survivor 或 Old Region。

b) 逐元素分解

  • GC 前:Eden 和 From-Survivor 中有存活对象,To-Survivor 为空。
  • 并行 GC 线程:工作窃取模型分配复制任务,多线程并发执行。
  • GC 后:存活对象按年龄分流至 To-Survivor 或 Old,原 Region 清空。
  • 年龄晋升:固定阈值与动态判定共同决定晋升时机。

c) 设计原理映射:Young GC 的并行化基于工作窃取,最大化 CPU 利用率,缩短 STW 时间。动态年龄判定是自适应策略,根据实际数据分布自动调整晋升阈值,避免静态阈值导致的 Survivor 空间浪费或过度晋升。

d) 工程联系与关键结论Young GC 的 STW 时间主要由存活对象数量和复制成本决定。若 Young GC 频繁超时,可减小 -XX:G1MaxNewSizePercent 限制年轻代上限,减少单次回收量;若过于频繁但停顿远低于目标,可适当调大以提升吞吐。G1 的自适应策略会自动进行这些调整,人工干预通常是最后手段。

6.4 动态年轻代大小调整

G1 内部维护一个反馈控制器,根据近期 GC 的实际停顿与 -XX:MaxGCPauseMillis 目标的偏差,动态调整年轻代 Region 数量。若实际停顿持续超过目标,减小年轻代;若远低于目标,增大年轻代。调整范围由 -XX:G1NewSizePercent-XX:G1MaxNewSizePercent 界定。此逻辑位于 g1CollectorPolicy.cppG1Policy::record_collection_pause_end()G1AdaptiveIHOP 相关类中。


第七章 Humongous Region:大对象分配与 Cleanup 阶段回收

7.1 分配条件与流程

对象大小达到 Region 大小的 50% 时,被判定为巨型对象。分配时调用 G1CollectedHeap::humongous_obj_allocate(),计算所需连续 Region 数量 N,从空闲列表查找连续 N 个空闲 Region。分配成功后,起始 Region 标记为 Humongous Starts,后续标记为 Humongous Continues,对象横跨整个连续区域。

若找不到足够连续空间,G1 将触发一次 Full GC 执行全堆压缩,整理碎片后重试分配。因此,频繁分配不同大小的巨型对象极易导致碎片化,引发 Full GC。

7.2 回收时机

巨型对象不参与任何复制回收,原因有二:一是复制成本太高,二是其占用空间可能横跨多个 Region,无法放入单个 Survivor Region。其空间回收仅有两个时机:

  • Cleanup 阶段:并发标记周期结束时,若判定巨型对象死亡,其连续 Region 组整体归还空闲列表。
  • Full GC:全堆压缩时回收所有死巨型对象并重整碎片。

因此,短命的巨型对象(如临时大数组)会长时间占据老年代空间,直到下一个并发标记周期完成才能释放。这是 G1 设计的一个重要缺陷,在 ZGC 和 Shenandoah 中得到解决。

flowchart TD
    A["分配请求: 对象大小 >= Region的50%"] --> B{"老年代有足够连续空间?"}
    B -- "是" --> C["分配 Humongous Starts + N-1 个 Continues"]
    B -- "否" --> D["Full GC 压缩后重试"]
    C --> E["对象存活,不参与 Young/Mixed GC"]
    E --> F{"并发标记周期结束 Cleanup 阶段"}
    F -- "对象已死" --> G["整体回收 Region 组"]
    F -- "对象仍存活" --> E
    D --> C
    classDef process fill:#f1f5f9,stroke:#334155,color:#1e293b;
    classDef decision fill:#ede9fe,stroke:#8b5cf6,color:#4c1d95;
    class A,C,D,E,G process;
    class B,F decision;

a) 主旨概括:该图说明巨型对象从分配、存活到最终回收的路径,重点标记了其回收滞后的特性。

b) 逐元素分解

  • 50% 阈值:判定标准,由 Region 大小决定。
  • 连续空间查找:可能因碎片化触发 Full GC。
  • 不参与复制回收:核心特征,直接后果是回收延迟。
  • Cleanup 回收:唯一常规回收时机,依赖并发标记周期。

c) 设计原理映射:这是对“避免复制开销”和“回收及时性”的经典权衡。G1 选择前者,牺牲后者,对短命大对象极不友好。这一设计选择在后续的 ZGC 中被修正。

d) 工程联系与关键结论生产环境中应严格避免频繁分配短命巨型对象。可通过 GC 日志中 “Humongous Allocation” 和 “Full GC” 的关联出现来定位此类问题。解决方案包括代码层面优化(对象池复用、减小对象大小)或调整 -XX:G1HeapRegionSize 以改变巨型对象判定阈值。若应用内存模型本身就依赖大量大对象,应考虑升级到 ZGC 或 Shenandoah。


第八章 参数调优与工程实战

G1 的参数调优围绕 -XX:MaxGCPauseMillis 这一核心目标展开。下表汇总了 G1 的八项关键参数,涵盖默认值、控制目标与调优原则,后续逐项展开详细分析。

参数默认值控制目标调优原则
-XX:MaxGCPauseMillis200ms所有 STW 停顿的期望上限调优北极星,设置需符合应用延迟需求,不宜过低
-XX:G1HeapRegionSize堆大小/2048 取整为 2 的幂Region 大小与总数优先调优,目标约 2048 个 Region,大堆可适当增大
-XX:InitiatingHeapOccupancyPercent45并发标记触发阈值Full GC 频繁时降低,CPU 过高时提高;JDK 9+ 自适应
-XX:G1MixedGCCountTarget8Mixed GC 执行次数停顿超时增大,回收不及时减小
-XX:G1MixedGCLiveThresholdPercent85Old Region 进入 CSet 的存活率上限回收效果差降低,老年代堆积提高
-XX:G1ReservePercent10晋升失败安全缓冲空间晋升失败频繁时增大
-XX:ConcGCThreadsParallelGCThreads/4并发标记线程数Remark 阶段耗时长时增大,需预留 CPU 给应用线程
-XX:ParallelGCThreadsCPU 核数 ≤8 时等于核数,否则 8+(核数-8)×5/8STW 阶段并行 GC 线程数过高导致上下文切换和内存带宽争用

8.1 -XX:MaxGCPauseMillis(默认 200ms)

这是 G1 调优的北极星参数。G1 所有自适应机制——年轻代大小调整、IHOP 阈值推算、CSet 贪心选取、Mixed GC 次数分摊——均以趋近此值为目标运转。它并非硬性保证每次停顿都低于该值,而是作为停顿预测模型和反馈控制器的设定目标。

设置原则

  • 在线服务(如 RPC 接口、Web 请求):通常设为 100-200ms,与接口超时时间匹配。
  • 近实时处理(如消息队列消费者、流计算):可放宽至 200-500ms。
  • 批处理系统:可设为 500ms 以上或直接使用 Parallel 收集器。

常见误区:将该参数设为极小值(如 10ms)。若设置过低,G1 会过度缩小年轻代和 CSet 以匹配目标,导致单次回收量过小,回收速率跟不上分配速率,老年代持续堆积,最终触发 Full GC,造成远超目标的停顿。正确的设定应基于应用实际延迟 SLO,并留有一定余量。


8.2 -XX:G1HeapRegionSize(默认自动推导)

Region 大小是影响 G1 全局行为的结构性参数,应在所有参数调优中优先确定。默认推导公式为 heap_size / 2048,向上取整为最接近的 2 的幂,范围 1MB~32MB。

设置原则

  • 小堆(<4GB):使用 1MB 或 2MB,保持足够的 Region 数量以维持回收粒度。
  • 中等堆(4-16GB):使用 2MB 或 4MB,接近默认推导值。
  • 大堆(16-64GB):使用 8MB 或 16MB,减少 Region 总数以降低 RSet 内存开销和扫描成本。
  • 超大堆(>64GB):可使用 16MB 或 32MB,但需注意 Humongous 对象阈值随之升高(≥Region/2),可能导致更多对象走正常晋升路线。

调优信号

  • Region 过小:RSet 内存占用高,进程 RSS 膨胀,Mixed GC 的 “Scanning Remembered Sets” 阶段耗时长。
  • Region 过大:内部碎片增加,Mixed GC 回收粒度粗,单次回收 Region 数少但每个 Region 回收时间长,难以精细控制停顿。

8.3 -XX:InitiatingHeapOccupancyPercent(IHOP,默认 45)

IHOP 控制并发标记周期的启动时机。当整堆占用比例达到该阈值时,G1 启动并发标记。默认 45% 为 SATB 标记完成和 Mixed GC 回收预留了约 55% 的堆空间。

调优原则

  • 降低 IHOP(如 35-40%):提前启动标记,为 SATB 队列处理和 Mixed GC 回收留足时间窗口,降低 Full GC 风险,但增加并发标记频率和 CPU 开销。适用于内存增长迅速、浮动垃圾多的应用。
  • 提高 IHOP(如 50-55%):延迟标记启动,减少并发标记周期频次,降低 CPU 开销,但压缩了标记和回收的时间窗口,增加 Full GC 风险。适用于内存增长平缓、对象生命周期长的应用。

自适应 IHOP:JDK 9+ 默认开启 -XX:+G1UseAdaptiveIHOP,根据历史分配速率和标记耗时动态推算“最晚安全启动点”。自适应模式通常在大多数场景下表现良好,仅在自动策略失效时手动干预。关闭自适应可通过 -XX:-G1UseAdaptiveIHOP

诊断信号:GC 日志中出现 “to-space overflow” 或 “Full GC (Allocation Failure)”,且堆未满,说明 IHOP 过高;并发标记周期频繁启动但无 Full GC,CPU 占用持续偏高,说明 IHOP 可能过低。


8.4 -XX:G1MixedGCCountTarget(默认 8)

控制一轮并发标记周期结束后 Mixed GC 的执行次数。候选 Old Region 的回收被分摊到这些 Mixed GC 中完成。

调优原则

  • 增大该值(如 12-16):每次 Mixed GC 回收更少的 Old Region,单次停顿更短,但总回收周期延长。适用于停顿超时频繁、CPU 资源充裕的场景。
  • 减小该值(如 4-6):每次 Mixed GC 回收更多的 Old Region,总回收周期缩短,但单次停顿增加。适用于老年代回收速度跟不上分配速度、堆积严重的场景。

MaxGCPauseMillis 的联动:若 Mixed GC 实际停顿持续超过目标值,增大 G1MixedGCCountTarget 是首选的优化手段——它将回收工作进一步打散,使单次停顿更容易满足目标。


8.5 -XX:G1MixedGCLiveThresholdPercent(默认 85)

Old Region 进入 CSet 候选集合的存活率上限。存活对象占比超过该值的 Region 被排除,因为回收它们释放的空间有限而复制开销高。

调优原则

  • 降低该值(如 65-75%):只回收存活率低、垃圾多的 Region,回收效率高,单次停顿释放空间多。但可能遗漏部分有回收潜力的 Region,导致老年代逐渐堆积。
  • 提高该值(如 90-95%):尝试回收更多 Region,包括那些几乎存活的 Region。增加停顿消耗,可能回收效率低下(复制大量存活对象仅释放少量空间)。

诊断信号:若 Mixed GC 后老年代空间释放不明显,说明选中的 Region 存活率过高,应降低该阈值。若老年代占用持续上升、候选 Region 堆积,说明回收覆盖不足,可适当提高阈值。


8.6 -XX:G1ReservePercent(默认 10)

预留堆空间百分比,用于 Mixed GC 期间晋升对象的安全缓冲。当 Mixed GC 复制存活对象时,若老年代剩余空间不足,G1 可从预留空间中分配,避免晋升失败(Promotion Failure)触发的 Full GC。

调优原则

  • 增大该值(如 15-20%):为晋升预留更多空间,降低晋升失败风险,但减少可用堆空间。适用于晋升量大、Mixed GC 期间频繁触发 Full GC 的场景。
  • 减小该值(如 5%):增加可用堆空间,但降低晋升安全缓冲。仅适用于晋升稳定、从未出现晋升失败的应用。

诊断信号:GC 日志中出现 “to-space overflow” 或 “Full GC (Promotion Failure)”,且老年代未完全用尽,说明晋升时预留空间不足,应增大此值。


8.7 -XX:ConcGCThreads(默认约为 ParallelGCThreads/4

并发标记线程数,负责并发标记阶段的对象图遍历和 SATB 队列消费。

调优原则

  • 增大该值:加快 SATB 队列消费速度,减少 Remark 阶段 STW 耗时,但增加并发标记期间的 CPU 占用。适用于 Remark 阶段耗时长、CPU 资源充裕的场景。
  • 减小该值:降低并发标记的 CPU 开销,但可能导致 SATB 队列积压,Remark 阶段耗时增加。适用于 CPU 资源紧张、应用线程对 CPU 竞争敏感的场景。

诊断信号:GC 日志中 “Pause Remark” 阶段耗时显著增长,说明 SATB 队列积压严重,并发标记线程处理不过来。此时可增大 ConcGCThreads 或降低 IHOP 提前标记开始时间。


8.8 -XX:ParallelGCThreads(默认自动推导)

STW 阶段(Young GC、Mixed GC、Full GC)的并行 GC 线程数。默认值为:CPU 核数 ≤8 时等于核数,否则为 8 + (核数 - 8) × 5/8

调优原则

  • 增大该值:加快 STW 阶段的回收速度,缩短停顿时间,但过多线程会导致上下文切换和内存带宽争用,反而降低效率。
  • 减小该值:减少 GC 线程对应用线程的 CPU 抢占,但延长 STW 停顿。

设置建议:对于容器化环境,注意 JVM 默认检测的是宿主机 CPU 核数而非容器限制,需通过 -XX:ActiveProcessorCount 或容器感知的 JDK 版本(JDK 10+)纠正。


8.9 调优闭环流程

G1 调优是一个基于监控数据的闭环迭代过程:

  1. 设定基线-Xms-Xmx 设为相同值,开启 GC 日志(-Xlog:gc*:file=gc.log:time,level,tags),设定合理的 MaxGCPauseMillis 目标。
  2. 确定 Region 大小:根据堆大小选定 G1HeapRegionSize,使 Region 总数接近 2048。
  3. 观察 Young GC:关注停顿是否接近目标,频率是否合理。自适应策略通常能自行优化,人工干预仅在长期偏离时进行。
  4. 观察 Mixed GC:关注停顿和回收效果。若停顿超时,增大 G1MixedGCCountTarget;若回收效果差,降低 G1MixedGCLiveThresholdPercent
  5. 排查 Full GC:分析 Full GC 触发原因(晋升失败、并发标记失败、巨型对象分配失败),针对性调整 IHOP、G1ReservePercent 或 Region 大小。
  6. 监控 Humongous 对象:若 GC 日志显示频繁的 Humongous Allocation 和关联 Full GC,从代码层面优化大对象使用,或调整 Region 大小改变巨型对象判定阈值。
  7. 迭代收敛:每次仅调整一个参数,观察调整后的 GC 行为变化,逐步收敛至最优配置。

关键原则:每次只改动一个参数,压测观察效果后再进入下一轮调优。多参数同时调整会模糊因果关系,无法定位真正的性能瓶颈。


8.10 特殊场景下的收集器选择

当 G1 调优无法满足延迟需求时,需评估是否升级收集器:

  • RSet 扫描瓶颈持续无法缓解(引用网络高度互联、Coarsening 严重):考虑 ZGC。ZGC 基于染色指针,完全移除 RSet,停顿与堆大小和引用复杂度无关。
  • Humongous 对象频繁分配与回收(大量短命大对象):考虑 ZGC 或 Shenandoah。两者支持大对象的并发移动和回收,彻底消除 G1 的 Humongous 痛点。
  • 超大堆(>32GB)且延迟目标极低(<10ms):ZGC 是目前最成熟的选择,停顿时间稳定在亚毫秒级,且吞吐量随 JDK 版本迭代不断优化。

G1 仍是大多数服务器端应用的最佳默认选择——它在吞吐量与延迟之间提供了最广泛的适用窗口。只有在 G1 的性能天花板被明确触及时,才需考虑迁移到 ZGC 或 Shenandoah。


面试高频专题

(题目1)G1 的 Region 有哪几种类型?Eden、Survivor、Old、Humongous 分别用于什么场景?

一句话回答:G1 将堆划分为等大 Region,类型动态变化,分为 Eden(新对象分配)、Survivor(存放年轻代 GC 存活对象)、Old(存放长期存活对象)、Humongous(存放超过 Region 一半大小的巨型对象)。

详细解释:Region 类型由 HeapRegion::_type 字段标识。Eden 是年轻代分配区,TLAB 在其中运作;Survivor 是 Young GC 后存活对象暂存区,有 From/To 之分;Old 是长期存活对象区域,是 Mixed GC 的核心回收目标;Humongous 是巨型对象专用区,由一组连续 Region 构成,分配时直接进老年代,回收仅在 Cleanup 或 Full GC。

多角度追问

  • 为何 Region 大小必须是 2 的幂? 答:便于通过位运算快速定位对象所属 Region 和 Card,提升屏障和根枚举性能。
  • TLAB 在 Region 内如何实现? 答:线程在 Eden Region 中通过指针碰撞分配私有 TLAB,仅当 TLAB 耗尽时才向堆申请新 TLAB,减少锁竞争。
  • Young GC 时 Survivor 空间不足怎么办? 答:存活对象将直接晋升到 Old Region,可能触发预期外的老年代增长。

加分回答:Region 动态角色模型是 G1 区别于传统固定分代的核心。它允许 G1 将回收粒度从“整个分代”缩小到“部分 Region”,使停顿时间可控。这一思想在 ZGC 中被进一步发展为“基于 Page 的分区回收”,但 ZGC 的 Page 角色更加灵活,支持并发移动。

(题目2)G1 的 SATB 并发标记算法是如何工作的?pre-write barrier 记录旧值的目的是什么?

一句话回答:SATB 在并发标记开始时建立对象图逻辑快照,由 pre-write barrier 将引用更新前的旧值记录到 SATB 队列,并发标记线程追踪旧值,保证快照时刻存活对象均被标记,逻辑上不漏标。

详细解释:pre-write barrier 在引用字段赋值前执行,读取旧值,若并发标记活跃且旧值非空,则推入线程本地 SATB 缓冲区。缓冲区满后移交全局队列,并发标记线程从全局队列取旧值并递归追踪其引用。Remark 阶段 STW 清空所有残留。该策略基于快照原子性,避免追踪并发修改,代价是产生浮动垃圾。

多角度追问

  • SATB 队列满会怎样? 答:本地缓冲区满移交全局队列;全局队列积压导致 Remark 阶段耗时增加,极端情况下触发中间 STW 辅助处理。
  • 初始标记为何与 Young GC 合并? 答:复用 Young GC 的 STW 和根扫描,减少独立停顿。
  • 浮动垃圾对什么应用不友好? 答:大量朝生夕死对象的应用,浮动垃圾迅速占用老年代,可能导致过早 Full GC。

加分回答:SATB 的正确性基于 Dijkstra 的并发标记算法,其形式化证明可追溯到 1970 年代的 on-the-fly garbage collection 研究。HotSpot 的 SATBMarkQueue 使用线程局部队列 + 工作窃取,将并发开销分摊到多核,是工业级实现典范。

(题目3)SATB 与 CMS 的增量更新在并发标记策略上有什么区别?为什么 SATB 能保证不漏标但浮动垃圾更多?

一句话回答:SATB 采用快照前视角,pre-write barrier 记录旧值,确保快照时刻可达对象必被标记,不漏标,代价是浮动垃圾;CMS 增量更新采用修改后视角,post-write barrier 记录新值,在 Remark 阶段修正,存在漏标风险,但浮动垃圾少。

详细解释:CMS 的增量更新关注“黑色对象新增指向白色对象的引用”,通过 post-write barrier 将新引用的目标或卡标记为脏,Remark 时重新扫描。但如果“灰色对象删除指向白色对象的引用”同时“黑色对象新增指向该白色对象的引用”,CMS 可能漏标。SATB 通过快照消除了此类追踪,快照中的存活对象无论后续引用如何变化均被标记,因此不漏标。但快照后死亡的对象本次不回收,浮动垃圾增多。

多角度追问

  • CMS 如何尝试解决漏标? 答:通过多轮预清理(Precleaning)和最终 Remark,但仍然有极小概率漏标,可能导致 Concurrent Mode Failure。
  • SATB 的写屏障开销比 CMS 大多少? 答:SATB 需要读取旧值并入队,比 CMS 的简单卡标记重约 5%-15% 的 CPU 开销。
  • 什么场景下 CMS 的漏标风险最高? 答:高度并发、引用变更频繁的场景,如高并发缓存更新。

加分回答:这两种策略代表了并发 GC 的两种哲学:CMS 的“保守回收”(少浮动垃圾,多复用空间)和 G1 的“安全回收”(宁可多浮动垃圾,绝不漏标)。生产环境中,G1 凭借 SATB 的健壮性,在延迟敏感型应用中对 Full GC 的免疫力显著优于 CMS,这也是 JDK 9 将 G1 设为默认收集器的重要原因。

(题目4)G1 的 RSet 是什么?它如何记录跨 Region 引用?Sparse、Fine、Coarsening 三级粒度分别是什么?

一句话回答:RSet 是每个 Region 维护的记录外部 Region 指向自身引用的数据结构,通过 post-write barrier 和异步 Refine 线程更新,内部采用 Sparse(哈希表)、Fine(Card 位图)、Coarsening(Region 位图)三级粒度在空间和时间开销间权衡。

详细解释:post-write barrier 将跨 Region 引用的源 Card 标记为脏,Refine 线程异步扫描脏卡,解析精确引用并更新目标 Region 的 RSet。Sparse 是哈希表存 Card 索引,Fine 是为每个外部 Region 维护的 Card 位图,Coarsening 是退化为全局 Region 位图(只记录“哪个外部 Region 引用了本 Region”)。升级由表项数或内存占用触发。

多角度追问

  • 为何 RSet 更新要异步? 答:同步更新需要全局锁,严重拖慢应用线程。异步化将开销转移给并发 Refine 线程。
  • Coarsening 影响多大? 答:扫描时间从微秒级骤升至毫秒级,成为 Mixed GC 停顿瓶颈。
  • RSet 和卡表的关系? 答:卡表是全局“脏页”索引,RSet 是按 Region 隔离的精确引用目录,建立在卡表之上。

加分回答:RSet 内存占用是 G1 堆外内存消耗的大头。-XX:G1RSetRegionEntries 控制 Sparse 表最大条目数。生产中出现进程 RSS 过高而堆内存未满,往往是 RSet 膨胀。调大 G1HeapRegionSize(减少 Region 总数)可有效降低 RSet 总量。

(题目5)RSet 的写屏障维护开销为什么比 CMS 的卡表更新更重?Coarsening 退化为位图后有什么影响?

一句话回答:G1 的 post-write barrier 不仅标记卡表,还需触发 RSet 更新(Refine 线程异步处理、锁竞争、RSet 条目插入),开销比 CMS 的简单卡标记更重;Coarsening 后 RSet 内存占用量降至最低,但回收该 Region 时需扫描整个被记录的外部 Region,停顿时间剧增。

详细解释:CMS 的 post-write barrier 仅做一次 dirty_card 操作,是简单内存写。G1 的 barrier 要检查是否跨 Region,若是,则将脏卡加入 Refine 队列。Refine 线程的处理涉及:解析脏卡内对象、查找跨 Region 引用、操作目标 Region 的 RSet(可能触发升级或退化)。这整套机制直接拉低了 G1 的吞吐量上限。Coarsening 后,回收 Region 时 GC 线程需线性扫描所有被记录的外部 Region,即使其中绝大部分引用都已消失。

多角度追问

  • JIT 如何优化 G1 写屏障? 答:内联到赋值点,对不逃逸对象可消除屏障。
  • 如何监控 Coarsening? 答:使用 debug 版 JVM 的 -XX:+PrintRSets 观察;线上通过 Mixed GC 的 “Scanning Remembered Sets” 耗时异常增长推断。
  • ZGC 如何避免 RSet 开销? 答:使用染色指针和读屏障,完全抛弃 RSet,但引入读屏障开销。

加分回答:G1 写屏障的跨平台优化是 HotSpot 中最复杂的部分之一。x86 实现利用 lock 前缀指令的原子性高效进行卡标记和队列操作,ARM 上使用屏障指令保证内存序。Coarsening 本质是内存预算技术,当 RSet 总量超过上限时,主动牺牲部分 Region 的回收效率以保护整个进程的内存安全。

(题目6)Mixed GC 的 CSet 是如何选择的?Garbage First 的“回收价值”公式是什么?

一句话回答:CSet 选择基于贪心算法,按回收价值(垃圾量 / 预测回收时间)对候选 Old Region 降序排列,依次选取,直至累计预测时间达到 MaxGCPauseMillis 目标。

详细解释:候选 Region 首先过滤存活率超过 G1MixedGCLiveThresholdPercent 的。价值公式为 Value = (Region总字节 - 存活字节) / predict_region_elapsed_time(hr)。预测函数基于历史存活字节数和衰减平均,估算回收该 Region 需时。贪心选取使单位停顿回收垃圾量最大化,实现 “Garbage First”。

多角度追问

  • 若一个 Region 垃圾很多但耗时也长? 答:贪心算法看性价比,若其价值仍高会被选中。G1MixedGCCountTarget 分摊回收,避免单次吃下“大块头”。
  • 预测模型数据如何衰减? 答:衰减平均,越近 GC 权重越高,快速适应负载变化。
  • G1MixedGCLiveThresholdPercent 如何起作用? 答:作为第一道筛选门槛,高于该值的 Region 不参与价值排序,直接排除。

加分回答:JDK 12 的 Abortable Mixed GC 允许在 Mixed GC 执行过程中,若实际耗时超过预期,可主动中止剩余 Region 的回收,将未回收 Region 留至下一次。这为预测模型失灵提供了安全网,进一步降低超时风险。

(题目7)G1 的停顿预测模型是如何工作的?它如何保证停顿不超过 MaxGCPauseMillis?

一句话回答:停顿预测模型基于历史 GC 数据(存活字节数、RSet 大小等),使用衰减平均预测每个 Region 的回收耗时,CSet 选择时累加预测值,确保总和不超过目标,它不“保证”,而是“努力趋近”。

详细解释G1Analytics::predict_region_elapsed_time_ms() 基于单位字节复制成本、RSet 扫描成本和固定开销,乘以 Region 的存活字节数和 RSet 大小,得出预测值。单位成本通过衰减平均从最近 GC 中学习。CSet 选择时严格执行总预测时间 ≤ MaxGCPauseMillis 的限制。

多角度追问

  • 预测模型总是低估怎么办? 答:可能是突发 Coarsening 或存活对象分布突变。JDK 12 的 Abortable Mixed GC 是补救。根本优化需减少 RSet 退化。
  • Young GC 也受该模型控制吗? 答:间接控制,通过自适应年轻代大小使 Young GC 停顿趋近目标。
  • MaxGCPauseMillis 可设为极小值吗? 答:若设置过低,G1 每次只能回收极少垃圾,可能跟不上分配速率,导致 Full GC。

加分回答:G1 停顿模型是典型的反馈控制闭环。MaxGCPauseMillis 为设定值,实际 GC 停顿为反馈,衰减平均为控制器,CSet 选择和年轻代调整为执行器。这种设计在大型分布式系统和数据库缓冲管理中具有普适性,是自适应控制的经典工程应用。

(题目8)Young GC 在 G1 中是如何执行的?它如何与 Mixed GC 共享 GC 线程?动态年轻代大小调整的依据是什么?

一句话回答:Young GC 为 STW 多线程并行复制,Mixed GC 复用同一套 GC 线程池和任务队列;动态年轻代调整基于历史停顿与 MaxGCPauseMillis 的偏差,由反馈控制器自动缩放年轻代 Region 数量。

详细解释:两者均调用 G1CollectedHeap::do_collection,内部通过 G1ParScanThreadState 等任务队列分派复制工作。Mixed GC 的 CSet 包含 Young Region,因此 Young GC 是其子集。动态调整位于 G1Policy::record_collection_pause_end(),若连续多次停顿超目标,则减小年轻代,反之增大。

多角度追问

  • Mixed GC 期间会回收 Young Region 吗? 答:会,所有 Young Region 都在 CSet 中,与普通 Young GC 回收方式相同。
  • ParallelGCThreads 设多少合适? 答:通常设为 CPU 核数除以 8,最大不超过 8,需根据实际负载压测确定。
  • 晋升对象如何选择目标 Old Region? 答:从空闲列表获取,使用 CAS 操作保证多线程安全。

加分回答:年轻代自适应策略的核心是“停顿预算分配”。G1 将 MaxGCPauseMillis 视为一个时间预算,在 Young GC 和 Mixed GC 之间动态分配。通过调整年轻代大小,G1 控制 Young GC 消耗的预算,为 Mixed GC 回收老年代留出余量。

(题目9)Humongous Region 是如何分配和回收的?为什么它只在 Cleanup 阶段或 Full GC 时回收?

一句话回答:巨型对象大小超 Region 一半,直接在老年代分配连续 Region 组,跳过年轻代;回收仅在 Cleanup 阶段(判定死亡后整体释放)或 Full GC(全堆压缩),因其不参与复制回收以避免移动代价。

详细解释humongous_obj_allocate() 在空闲列表中查找连续 Region,失败触发 Full GC。对象存续期间不参与 Young/Mixed GC 的复制,仅当并发标记判定其死亡后,在 Cleanup 阶段释放连续 Region 组。这导致短命巨型对象长期占用空间。

多角度追问

  • 短命大对象有何危害? 答:迅速占满老年代,导致晋升失败和 Full GC。
  • 如何定位短命大对象? 答:GC 日志 “Humongous Allocation” 和 Full GC 关联出现,或使用 JFR 的 Heap Statistics 事件。
  • 调整 G1HeapRegionSize 能解决吗? 答:增大 Region 可降低巨型对象判定门槛,使部分原 Humongous 对象变为普通对象,走正常晋升路线。

加分回答:ZGC 和 Shenandoah 通过并发移动和染色指针/转发指针,允许大对象在并发标记期间被移动和回收,彻底消除了 Humongous 痛点。这代表 GC 技术从“回避移动”到“并发移动”的范式转移。

(题目10)-XX:InitiatingHeapOccupancyPercent(IHOP)参数的作用是什么?默认 45% 是如何影响并发标记触发时机的?

一句话回答:IHOP 是触发并发标记周期的堆占用率阈值。默认 45% 意味着堆占用 45% 时启动标记,为标记完成和 Mixed GC 回收预留 55% 空间。

详细解释:IHOP 过高会导致标记来不及完成,老年代填满触发 Full GC;过低则标记周期过于频繁,浪费 CPU。自适应 IHOP(JDK 9+)根据分配速率和标记耗时动态调整,使标记在安全的最晚时间点触发。

多角度追问

  • 如何判断 IHOP 是否合理? 答:看 Full GC 频率和原因。若 “to-space overflow” 频繁,IHOP 可能太高;若并发标记周期频繁但无 Full GC,CPU 高,IHOP 可能太低。
  • G1 有自适应 IHOP 吗? 答:JDK 9 起 -XX:+G1UseAdaptiveIHOP 默认开启,自动调整。
  • IHOP 与 G1ReservePercent 关系? 答:IHOP 是提前预警,G1ReservePercent 是最后防线,共同防止晋升失败。

加分回答:自适应 IHOP 基于统计学控制图理论,使用反馈环监控分配率和标记速度,动态计算“最晚安全触发点”。其算法与 TCP 拥塞控制中的滑动窗口调整有异曲同工之妙,体现基础软件设计中的共性优化思想。

(题目11)-XX:G1MixedGCCountTarget-XX:G1MixedGCLiveThresholdPercent 分别控制什么?如何调优?

一句话回答G1MixedGCCountTarget 控制一轮标记后 Mixed GC 次数,分摊回收压力;G1MixedGCLiveThresholdPercent 设 Old Region 进入 CSet 候选的最大存活率,过滤低价值 Region。

详细解释:前者默认 8,增大可减少单次停顿,延长总回收时间;减小则加快回收,但单次停顿增加。后者默认 85%,增大可尝试回收更多 Region(可能低效),减小则只回收高价值 Region(可能不够激进)。调优需结合停顿日志和老年代占用趋势。

多角度追问

  • 什么场景应增大 G1MixedGCCountTarget 答:Mixed GC 频繁超时时,且 CPU 资源充裕。
  • 什么场景应降低 G1MixedGCLiveThresholdPercent 答:Mixed GC 回收效果差,释放空间少时。
  • 两者如何配合? 答:高 G1MixedGCCountTarget 单次回收压力小,可适当提高阈值回收更多 Region;反之则降低阈值集中精力回收高价值 Region。

加分回答:这两个参数共同定义 Mixed GC 的“工作包”。理想调优是使每轮 Mixed GC 恰好消耗停顿预算,且回收的全是高价值 Region。这需要对应用的对象生命周期有深入理解,是 GC 调优的高级阶段。

(题目12)G1 的 Full GC 在什么情况下会发生?与 CMS 的 Serial Old Full GC 相比有什么不同?

一句话回答:G1 Full GC 发生在晋升失败、并发标记失败、巨型对象分配失败或 System.gc() 调用时;JDK 10 之前为单线程 Serial,之后改为并行,比 CMS 的 Serial Old Full GC 在大堆上停顿更短。

详细解释:触发条件包括:Mixed GC 期间老年代不足(晋升失败)、并发标记未在堆满前完成(并发标记失败)、巨型对象分配时连续空间不足、System.gc() 等。JDK 10 的 JEP 307 将 G1 Full GC 改为并行标记-整理,多线程并行执行,显著缩短大堆停顿时间。CMS 的 Full GC 始终是单线程 Serial Old。

多角度追问

  • JDK 11 G1 还有优化吗? 答:JEP 333 引入可中断 Mixed GC,进一步降低停顿超标风险。
  • 如何观察 Full GC 发生? 答:GC 日志中 “Pause Full (Allocation Failure)” 或 “G1 Full GC” 标记。
  • 如何遏制 Full GC? 答:调优 IHOP、G1ReservePercentG1HeapRegionSize,避免 Humongous 对象,禁用 System.gc()

加分回答:G1 并行 Full GC 是 HotSpot GC 并行化工程的收官之作。它将 Parallel Old 的并行压缩算法迁移到 G1 复杂的 Region 和 RSet 数据结构上,从单线程 20 年的历史包袱中解放出来。生产环境中升级到 JDK 11+ 可享受这一显著提升。

(题目13)故障排查题:线上服务使用 G1,MaxGCPauseMillis=200ms,偶现停顿超 1 秒的 Mixed GC,日志显示 CSet Region 数量少但每个 Region 回收时间长,“Scanning Remembered Sets” 耗时占比高。(a) 为什么 RSet 扫描成为瓶颈?(b) 如何调优 RSet?(c) 画出异常 Mixed GC 耗时分布;(d) 若瓶颈无法缓解,如何调整或升级?

一句话回答:目标 Region 的 RSet 发生严重 Coarsening,或引用其的外部 Region 过多,导致扫描 RSet 必须遍历大量外部 Region,成为瓶颈。

详细解释(a) 原因分析:RSet 扫描慢的根因是 Coarsening 或 Fine 粒度下引用极密集。Coarsening 后,G1 须扫描被记录的外部 Region 的每个对象来查找指向目标 Region 的引用。常见于:全局缓存被大量 Region 引用,或存活对象密集的 Region 晋升时携带大量跨 Region 引用。

(b) 参数调优:可尝试增大 -XX:G1RSetRegionEntries 延后 Fine 升级,但治标不治本。更有效的方法是增大 -XX:G1HeapRegionSize(减少 Region 总数,降低单 Region 被引用复杂度),或从代码层面减少“中心节点”对象对外的引用数量。

(c) 耗时分布图

flowchart LR
    subgraph AbnormalMixedGC ["一次异常Mixed GC 耗时分布,总停顿1.2s"]
        S0["开始"] --> S1["预清理 20ms"]
        S1 --> S2["扫描 RSet 阶段 800ms 瓶颈!"]
        S2 --> S3["对象复制 200ms"]
        S3 --> S4["引用处理/清理 100ms"]
        S4 --> S5["其他 80ms"]
    end

    classDef nodeStyle fill:#f1f5f9,stroke:#334155,color:#1e293b;
    classDef subStyle fill:#fef3c7,stroke:#d97706,color:#92400e;

    class S0,S1,S2,S3,S4,S5 nodeStyle;
    class AbnormalMixedGC subStyle;

(d) 根本性方案

  1. 增大 G1HeapRegionSize:减少 Region 总数,降低跨 Region 引用密度,RSet 内存和扫描成本显著降低。
  2. 降低 G1MixedGCLiveThresholdPercent:让 G1 避开那些几乎存活的、RSet 复杂的 Region,不回收它们以绕过扫描瓶颈。
  3. 升级到 ZGC:ZGC 基于染色指针,无 RSet,停顿与堆大小和引用复杂度无关,是解决 RSet 瓶颈的终极方案。

多角度追问

  • 如何线下复现 RSet 瓶颈? 答:使用 debug 版 JVM 的 -XX:+PrintRSets dump RSet 信息,定位 Coarsening 的 Region。
  • 增大 G1HeapRegionSize 为何减少跨 Region 引用? 答:Region 变大,两个有引用的对象更可能落在同一 Region 内,跨 Region 引用自然减少。
  • 代码层面如何规避? 答:排查“中心节点”对象,如全局缓存 Map,将其内部引用关系设计得更内聚,减少对外部对象的引用。

加分回答:此问题揭示了 G1 的性能天花板——当应用对象图呈高度互联的网状结构时,RSet 维护和扫描的开销将超过复制回收本身。此时,ZGC 的读屏障方案提供了 O(1) 级的引用追踪复杂度,是技术栈迁移的合理方向。这也解释了为何 JDK 同时维护 G1 和 ZGC 两个低延迟收集器:它们各有所长,面向不同的应用内存模型。