概述
前文《垃圾收集器全景: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 GC Region 化堆布局 + 复制回收:以 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.cpp 的 G1CollectedHeap::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.cpp 的 G1AdaptiveIHOP 类中,其核心是维护一个“标记周期耗时 + 并发期间预期分配量”的预测模型。
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 选择算法执行以下步骤:
- 候选过滤:存活对象占比超过
-XX:G1MixedGCLiveThresholdPercent(默认 85%)的 Old Region 被直接排除——回收价值过低。 - 价值排序:对剩余候选 Region,计算回收价值 = 可回收垃圾量 /
predict_region_elapsed_time_ms()。预测函数基于历史 GC 统计中的存活字节数与实际耗时,使用衰减平均模型估算回收该 Region 所需时间。 - 贪心选取:按回收价值降序排列,依次选入 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 的复制回收,其生命周期终结依赖两个时机:
- Cleanup 阶段:并发标记周期末尾的 Cleanup 阶段,若判定某巨型对象已无任何引用,其占用的连续 Region 组直接整体归还空闲列表。此为常规回收路径。
- 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.hpp 的 HeapRegion 类表示。其类型通过 _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.cpp 的 G1CollectedHeap::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 字节码层面,每个引用类型的字段赋值(putfield、putstatic、aastore)之前,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.hpp 和 satbQueue.cpp。SATBMarkQueueSet 管理一组线程本地队列,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 + 并发)。对比 nextBitmap 和 prevBitmap,计算每个 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 线程 协同完成:
- 当发生跨 Region 的引用赋值时,post-write barrier 将引用源所在的 Card 标记为脏(卡表标记)。
- 专门的 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.cpp 的 G1Policy::finalize_cset() 执行,其贪心算法步骤如下:
- 初始 CSet:包含所有 Young Region(Eden 和 From-Survivor)。Young Region 的回收是必须的,且其预测时间已从年轻代大小自适应中体现。
- 候选排序:将所有候选 Old Region 按回收价值降序排列。回收价值公式为:
分子为可回收垃圾量,分母为预测回收耗时。该值表征“单位停顿时间可回收的垃圾量”,值越大,回收性价比越高。Value = (Region_Total_Bytes - Live_Bytes) / predict_region_elapsed_time(hr) - 贪心选取:从价值最高的 Region 开始,逐个累加预测耗时,直至总预测时间达到
-XX:MaxGCPauseMillis目标,或候选集遍历完毕。 - 分摊控制:若一轮 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 工作线程并行执行以下步骤:
- 根扫描:扫描 GC Roots(线程栈、JNI 句柄、类静态变量、同步监视器等),找到直接可达对象。
- RSet 扫描:扫描 CSet 中每个 Region 的 RSet,获取所有从外部 Region 指向 CSet 的引用入口。这些入口也是根的一部分。
- 存活对象标记与复制:从根出发,递归标记所有存活对象,并将其复制到 To-Survivor Region 或 Old Region。复制过程采用工作窃取(work stealing)模型,GC 线程从共享任务队列获取复制任务,高效并行。
- 引用更新:所有存活对象复制完成后,更新指向这些对象的引用(使用“转发表”或直接修改)。
- 清理:清空 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.cpp 的 G1Policy::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:MaxGCPauseMillis | 200ms | 所有 STW 停顿的期望上限 | 调优北极星,设置需符合应用延迟需求,不宜过低 |
-XX:G1HeapRegionSize | 堆大小/2048 取整为 2 的幂 | Region 大小与总数 | 优先调优,目标约 2048 个 Region,大堆可适当增大 |
-XX:InitiatingHeapOccupancyPercent | 45 | 并发标记触发阈值 | Full GC 频繁时降低,CPU 过高时提高;JDK 9+ 自适应 |
-XX:G1MixedGCCountTarget | 8 | Mixed GC 执行次数 | 停顿超时增大,回收不及时减小 |
-XX:G1MixedGCLiveThresholdPercent | 85 | Old Region 进入 CSet 的存活率上限 | 回收效果差降低,老年代堆积提高 |
-XX:G1ReservePercent | 10 | 晋升失败安全缓冲空间 | 晋升失败频繁时增大 |
-XX:ConcGCThreads | ParallelGCThreads/4 | 并发标记线程数 | Remark 阶段耗时长时增大,需预留 CPU 给应用线程 |
-XX:ParallelGCThreads | CPU 核数 ≤8 时等于核数,否则 8+(核数-8)×5/8 | STW 阶段并行 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 调优是一个基于监控数据的闭环迭代过程:
- 设定基线:
-Xms与-Xmx设为相同值,开启 GC 日志(-Xlog:gc*:file=gc.log:time,level,tags),设定合理的MaxGCPauseMillis目标。 - 确定 Region 大小:根据堆大小选定
G1HeapRegionSize,使 Region 总数接近 2048。 - 观察 Young GC:关注停顿是否接近目标,频率是否合理。自适应策略通常能自行优化,人工干预仅在长期偏离时进行。
- 观察 Mixed GC:关注停顿和回收效果。若停顿超时,增大
G1MixedGCCountTarget;若回收效果差,降低G1MixedGCLiveThresholdPercent。 - 排查 Full GC:分析 Full GC 触发原因(晋升失败、并发标记失败、巨型对象分配失败),针对性调整 IHOP、
G1ReservePercent或 Region 大小。 - 监控 Humongous 对象:若 GC 日志显示频繁的 Humongous Allocation 和关联 Full GC,从代码层面优化大对象使用,或调整 Region 大小改变巨型对象判定阈值。
- 迭代收敛:每次仅调整一个参数,观察调整后的 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、
G1ReservePercent、G1HeapRegionSize,避免 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) 根本性方案:
- 增大
G1HeapRegionSize:减少 Region 总数,降低跨 Region 引用密度,RSet 内存和扫描成本显著降低。 - 降低
G1MixedGCLiveThresholdPercent:让 G1 避开那些几乎存活的、RSet 复杂的 Region,不回收它们以绕过扫描瓶颈。 - 升级到 ZGC:ZGC 基于染色指针,无 RSet,停顿与堆大小和引用复杂度无关,是解决 RSet 瓶颈的终极方案。
③ 多角度追问:
- 如何线下复现 RSet 瓶颈? 答:使用 debug 版 JVM 的
-XX:+PrintRSetsdump RSet 信息,定位 Coarsening 的 Region。 - 增大
G1HeapRegionSize为何减少跨 Region 引用? 答:Region 变大,两个有引用的对象更可能落在同一 Region 内,跨 Region 引用自然减少。 - 代码层面如何规避? 答:排查“中心节点”对象,如全局缓存 Map,将其内部引用关系设计得更内聚,减少对外部对象的引用。
④ 加分回答:此问题揭示了 G1 的性能天花板——当应用对象图呈高度互联的网状结构时,RSet 维护和扫描的开销将超过复制回收本身。此时,ZGC 的读屏障方案提供了 O(1) 级的引用追踪复杂度,是技术栈迁移的合理方向。这也解释了为何 JDK 同时维护 G1 和 ZGC 两个低延迟收集器:它们各有所长,面向不同的应用内存模型。