G1GC的算法与实现
G1GC的算法与实现
根据《深入Java虚拟机-JVM G1GC的算法与实现》整理而来,主要涵盖以下章节:
- 第一章 G1GC是什么?
- 第二章 并发标记
- 第三章 转移
- 第四章 软实时性
- 第五章 分代G1GC模式
- 第六章 算法篇总结
G1GC是什么?
Garbage-First (G1) 垃圾收集器是一种服务端的垃圾收集器,针对具有大内存和多处理器的机器。它尝试尽量满足用户设定的垃圾收集 (GC) 暂停时间,同时实现高吞吐量。GC操作(例如全局标记)与应用程序线程同时执行。
这是JDK官方对于G1的定义。G1的核心特点是软实时性:
- 硬实时性:硬性强制要求的,达不到指定时间就返回失败
- 软实时性:柔性的,设定时间只是期望,超出频率在容忍范围内即可
类比:公司允许每个考勤周期迟到2-3次(软实时性),但职务侵占一次都不能容忍(硬实时性)
G1支持4G以上的堆内存进行垃圾收集。
G1出现的背景
在G1出现之前,GC处理器主要通过增量GC或并发GC来减少STW停顿时间,但这会造成吞吐量下降。
G1的出现是为了在期望的暂停时间内尽量完成更大的GC吞吐量。目前GC的关注点更偏向于减少暂停时间,因为堆中的对象大多属于朝生夕死的类型。
G1的现状与演进
| JDK版本 | 变化 |
|---|---|
| JDK9 | G1成为默认垃圾回收器 |
| JDK14 | 增加NUMA(非统一内存访问)增强 |
| JDK14+ | 新增ZGC收集器,预计未来版本中作为默认GC |
截至JDK 20,G1GC仍是JDK平台跨最多版本的默认垃圾收集器。
G1GC的内存结构
Region:G1的核心创新
G1GC的内存布局最大的特点是抛弃了传统对老年代和年轻代的连续内存范围划分,而是采用**区域(Region)**的形式进行划分:
- 默认每个Region大小为1MB(可通过
-XX:G1HeapRegionSize设置,内部会向上调整为2的指数幂) - 堆被划分为约2048个Region
- 每个Region可以是 Eden / Survivor / Old / Humongous 四种类型之一
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ Eden │Survivor│ Old │ Eden │ Humon│ Old │ Free │ Eden │ ...
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
↑ 每个Region大小相等,角色可动态变化
Humongous区域:当对象大小超过一个Region容量的50%时,被视为大对象(Humongous Object),直接在连续的Humongous Region中分配。
跨代引用与记忆集
G1GC需要处理跨Region的对象引用关系,核心数据结构是记忆集(Remembered Set)和卡表(Card Table):
Region A (老年代) Region B (年轻代)
┌──────────────────┐ ┌──────────────────┐
│ obj1 ──────────────ref──────────→ obj3 │
│ obj2 │ │ obj4 │
└──────────────────┘ └──────────────────┘
│ ↑
Card Table Remembered Set
(标记哪个Card有跨代引用) (记录谁引用了我)
- Card Table:Java HotSpot VM 使用字节数组作为卡表,每个字节称为一张卡片(Card),对应堆中一段地址范围。"弄脏"一张卡片意味着该地址范围可能存在从老年代到年轻代的新指针
- Remembered Set:每个Region都有一个转移专用记忆集合,通过Hash列表实现。Key为引用本Region的其他Region地址,Value为对应卡表中的元素数组
RS的写入由专用的转移专用记忆集合维护线程负责,可与mutator线程并发执行。
G1GC的执行过程
所有GC在执行过程都可以划分为标记和整理两个大的步骤,G1GC底层算法是标记-压缩算法。
需要注意的是:并发标记和转移压缩在处理顺序上没有先后顺序,并发标记的结果对于转移压缩阶段也不是必须的。
一、并发标记阶段
标记位图
并发标记并不是直接在对象内存上添加标记,而是在**标记位图(Mark Bitmap)**上:
- 每个Region有两个标记位图:
nextBitMap(本次)和prevBitMap(上一次) - 每个bit位对应一个对象,默认最小对象为8字节
- 通过4个标记位确定标记范围:
bottom、top、prevTAMS、nextTAMS
Region内存布局:
bottom ────── prevTAMS ────── nextTAMS ────── top
│ │ │ │
│ 上次标记时 │ 本次标记时 │ 标记过程中 │
│ 已标记区域 │ 已标记区域 │ 新分配对象 │
│ │ │ (默认已标记) │
└───────────────┴───────────────┴────────────┘
nextTAMS ~ top 范围内的对象在标记过程中新产生,默认视为"已完成标记"。
标记五步骤
G1的并发标记过程分为五个步骤:
| 步骤 | STW? | 说明 |
|---|---|---|
| ① 初始标记 | 是 | 只标记GC Roots直接可达的对象(根扫描),写入nextBitMap |
| ② 并发标记 | 否 | GC线程与mutator线程并发执行,扫描上一步标记对象的引用关系 |
| ③ 最终标记 | 是 | 处理SATB本地队列中的残留引用 |
| ④ 存活对象计数 | 否 | 扫描next标记位图,统计各Region存活对象的字节数目 |
| ⑤ 收尾工作 | 否 | 汇总标记结果,为转移阶段做准备 |
初始标记阶段
- 创建Next MarkBitmap
- 对GC Root可达对象进行扫描和标记
- Mutator是暂停执行的(STW),因为mutator会频繁修改根对象引用,保证并发的性能损耗远大于顺序执行
- 只将根引用对象标记到标记位图中
并发标记阶段
GC线程继续扫描初始标记阶段被标记的对象,分析它们的引用关系,完成大部分存活对象的标记。
标记遗漏问题:
并发标记阶段GC线程与mutator线程并发执行,会产生标记遗漏。产生条件:
- 新产生一条从黑色对象(已标记)到白色对象(未标记)的新引用
- 删除灰色对象(正在标记)到白色对象的引用关系
| GC | 解决方案 | 破坏条件 | 原理 |
|---|---|---|---|
| CMS | 增量更新(Incremental Update) | 破坏条件① | 黑色对象插入新指向白色对象的引用时,记录下来,并发扫描结束后重新扫描 |
| G1 | 原始快照(SATB) | 破坏条件② | 灰色对象删除指向白色对象的引用时,记录下来,并发扫描结束后重新扫描 |
SATB (Snapshot At The Beginning):记录GC开始时的对象引用关系快照。会产生保守标记(将本可回收的对象标记为存活),但保证没有遗漏,防止错误回收。
G1GC通过**写屏障(Write Barrier)**技术记录引用关系变化。对于新分配的对象直接视为已标记完成。
SATB队列在实现上采用与线程绑定的形式,当本地队列装满时,刷新到全局SATB队列中。
最终标记阶段
处理SATB本地队列中的残留引用。因为并发标记结束后,本地SATB队列可能未满而不被刷新到全局队列,因此需要**全局暂停(STW)**来处理这些残留。
存活对象计数阶段
扫描各Region的next标记位图,统计各Region内存活对象的字节数目。
- 可与mutator并发执行
- 不能与转移线程(Remembered Set维护线程)并行执行,否则会破坏Region内的计数正确性
收尾阶段
汇总两组核心信息:
- 标记位图 → 存活对象和死亡对象的区分
- 存活对象字节数 → 各Region的内存占用
引入转移效率概念:转移效率 = 死亡对象的字节数 / 转移所需时间
通过历史转移效率数据,预测下次GC所需的回收时间。
二、转移压缩阶段
转移的目的是对内存进行整理,消除碎片化,使对象可以继续分配。
转移的先决条件
转移功能通过**转移专用记忆集合(Remembered Set)**快速索引跨Region的引用关系,即使不扫描所有Region内的对象,也能查到待转移对象被其他Region引用的情况。
转移三步骤
| 步骤 | 说明 |
|---|---|
| ① 选择回收集合 | 根据标记阶段信息,选择需要被转移的Region集合 |
| ② 根转移 | 将回收集合中由GC Root直接引用的对象和被其他Region引用的对象转移 |
| ③ 转移 | 以根转移的对象为起点扫描其子孙对象,全部转移 |
选择回收集合(G1的核心决策)
选择回收集合的两个原则:
- 转移效率高的Region优先(存活对象越少,转移效率越高)
- 整体转移预测的暂停时间在用户容忍范围内
1. 按转移效率对所有Region排序
2. 从高到低依次累积预测暂停时间
3. 累积时间达到用户设定的暂停时间阈值时停止
4. 选中的Region子集 = 本次回收集合(CSet)
G1 = Garbage First:转移效率从高到低的顺序就是"垃圾优先"的具体实现——垃圾越多的Region越优先回收。
根转移
转移三类数据:
- 由根直接引用的对象
- 并发标记处理中的对象
- 由其他Region对象直接引用的回收集合内的对象
对象转移的处理分类:
- 引用到回收集合内的对象 → 添加到转移队列,然后回收
- 引用到回收集合外的对象 → 更新其他对象的转移专用记忆集合
- 其他对象引用到回收对象时 → 更新回收对象的转移专用记忆集合
转移
根转移完成后,转移队列中被引用的对象依次进行转移,直到转移队列清空。至此,回收集合内所有存活对象都成功转移到其他Region。
软实时性
G1GC如何实现软实时性?
用户可以设置以下参数:
| 参数 | 说明 | 示例 |
|---|---|---|
-Xms / -Xmx |
堆空间最小/最大值 | -Xms4g -Xmx4g |
-XX:MaxGCPauseMillis |
GC暂停时间上限目标 | -XX:MaxGCPauseMillis=200(默认200ms) |
⚠️
-Xmn不建议使用,会破坏暂停时间上限的配置
G1GC根据预测转移时间和预测可信度来实现软实时性:
- G1GC内部有一个调度队列,保存最近暂停处理的开始时间和结束时间
- 调度队列有上限,超出时删除最早添加的元素
- 调度程序保证:在任意截取的GC单位时间内,总GC暂停时间不超过设定上限
调度过程:
- 预测下一次GC暂停时间
- 如果此时开始GC,在GC单位时间内会超过暂停时间上限 → 不暂停
- 延迟暂停时间,确保在GC单位时间内不超过上限 → 执行暂停
特殊情况下也会超出设定上限(GC预测不准确、堆内存不足等),这就是"软实时性"的含义。
分代G1GC模式
为什么要分代?
分代:通过给对象引入"年龄"来标记对象的重要程度,从而提升GC效率。基于"朝生夕灭"假说,大部分对象活不过一次GC。
GC模式
| 模式 | 说明 |
|---|---|
| 纯G1GC模式 | 不区分分代,仅按转移效率选择Region |
| 分代G1GC模式 | Region划分为新生代和老年代,分别处理 |
分代G1GC的不同点:
- 区域是分代的:新生代Region / 老年代Region
- 回收集合的选择是分代的
两种GC类型
| GC类型 | 回收集合 | 说明 |
|---|---|---|
| 完全新生代GC (Young GC) | 所有新生代Region | 只回收新生代 |
| 部分新生代GC (Mixed GC) | 所有新生代Region + 部分老年代Region | 同时回收新生代和部分老年代 |
新生代区域
新生代区域分为两类:
- 创建区域(Eden):存放刚生成的、未经历过转移的对象
- 存活区域(Survivor):存放至少转移过一次的对象
新生代中不会应用转移专用写屏障,因为新生代对象大多会被回收,引用方不会保存新生代的专用写屏障。
G1 vs CMS 对比
| 维度 | CMS | G1 |
|---|---|---|
| 目标 | 最短停顿时间 | 可控停顿时间 + 高吞吐量 |
| 算法 | 标记-清除(有碎片) | 标记-整理+复制(无碎片) |
| 内存布局 | 连续的老年代/年轻代 | Region化,灵活分配 |
| 并发标记方案 | 增量更新 | SATB原始快照 |
| 碎片问题 | 有,需要Full GC整理 | Region内无碎片 |
| 浮动垃圾 | 有 | 有(SATB保守标记导致) |
| 适用堆大小 | 中小堆(6G以下) | 大堆(4G以上) |
| 默认版本 | JDK8及之前 | JDK9及之后 |
G1GC调优参数速查
| 参数 | 说明 | 默认值 |
|---|---|---|
-XX:+UseG1GC |
启用G1GC | JDK9+默认启用 |
-XX:MaxGCPauseMillis |
目标最大暂停时间 | 200ms |
-XX:G1HeapRegionSize |
Region大小 | 根据堆大小自动计算 |
-XX:InitiatingHeapOccupancyPercent |
触发并发标记的堆占用阈值 | 45% |
-XX:G1ReservePercent |
保留内存防止晋升失败 | 10% |
-XX:ConcGCThreads |
并发标记线程数 | 根据CPU自动计算 |
-XX:ParallelGCThreads |
STW期间并行线程数 | 根据CPU自动计算 |
算法篇总结
G1GC执行全景:
Mutator线程 ═══════════════════════════════════════════════╗
↓ 并发标记开始 ↓ 转移开始
GC线程 ─ 初始标记 ─ 并发标记 ─ 最终标记 ─ 计数 ─ 转移 ──╣
(STW) (并发) (STW) (并发) (STW) ║
║
RS维护线程 ═══════════════════════════════════════╤════════╝
(计数时暂停)
G1GC的优点
- 具备软实时性,用户可控制GC暂停时间
- 能够充分发挥多核机器性能,实现并发执行
- 通过写屏障将处理粒度调整为卡片粒度,降低了写屏障发生的频率
- 通过对象转移,实现Region内无内存碎片
G1GC的缺点
- 适用于多核处理器设备,单核性能不如Serial
- Region内无碎片,但整个堆会按Region出现碎片化
- SATB保守标记会产生浮动垃圾
- Remembered Set维护带来额外的内存开销(通常占堆的10%~20%)
参考资料
- 《深入Java虚拟机:JVM G1GC的算法与实现》中村成洋(作者) 吴炎昌,杨文轩 (译者)
- 《垃圾回收的算法与实现》中村成洋,相川光,竹内郁雄 (作者) 丁灵 (译者)
- G1GC Feedback
- Java 8 vs Java 17 垃圾收集器
- G1GC最初的设计思路(上)
- JVM垃圾回收-记忆集和卡表
- 图解 Remembered Set、Card Table、Write Barrier
- Java Hotspot G1 GC的一些关键技术
- HotSpot Virtual Machine Garbage Collection Tuning Guide
- Concurrent Marking in G1
- 最清晰易懂的G1GC资料
- SATB的一些理解
- Write Barriers in Garbage First Garbage Collector