跳到主要内容
约 8 分钟4 张卡

G1

G1 是把堆划分为等大小区域、分批回收垃圾的收集器,这些区域称为 Region。回收时,它把选中区域的存活对象复制到其他区域,这个过程称为疏散。本文先讲 Region 与跨区域引用,再说明年轻代、混合回收和暂停目标,最后介绍日志分析。

1. 面试核心​

先记住这几条,再读正文
  • Region 角色。 G1 将堆划分为等大小的 Region,每个区域可按需承担 Eden、Survivor、Old 或 Humongous 角色。
  • 外部引用记录。 RSet 记录其他 Region 指向当前 Region 的引用摘要,让局部回收只扫描相关区域。卡 · Region
  • 混合回收。 Mixed GC 在年轻代回收中加入部分老年代 Region,这些候选区域来自并发标记的结果。
  • 暂停目标。 -XX:MaxGCPauseMillis=200 设置 200ms 的软暂停目标。G1 据此预测并选择回收工作量,实际停顿仍受存活量、扫描成本和调度影响。卡 · 暂停目标
  • 标记启动时机。 自适应 IHOP 根据标记耗时、老年代分配速率和预留空间,预测并发标记的启动时机。卡 · IHOP 与启动时机

2. Region​

Region 是 G1 分配和回收堆空间的基本区域,大小在 JVM 启动时确定。同一时刻,各个 Region 可以承担以下四种角色:

  • Eden 存放新分配的普通对象。
  • Survivor 存放年轻代回收后继续留在年轻代的对象。
  • Old 存放晋升等路径进入老年代的对象。
  • Humongous 存放达到大对象阈值的对象。
[Eden][Old][Free][Survivor][Old][Humongous][Humongous][Free]

年轻代和老年代分别由一组 Region 组成,所以各代的地址可以分散。G1 会根据暂停目标和当前负载调整年轻代的 Region 数量,从而改变年轻代容量。

2.1 Humongous 对象​

大对象需要特殊的分配方式,因为一个 Region 未必放得下它。JDK 21 HotSpot 把严格大于半个 Region 的对象视为 Humongous 对象。比较时使用包含对象头、完成对齐后的大小。这类对象占用一个或多个连续 Region,并直接归入老年代。

对象末尾所在 Region 的剩余空间,在对象回收前无法供其他对象分配。所以,大对象既需要连续区域,也会浪费尾部空间。大量对象刚好超过判定阈值时,这种浪费尤其值得检查。即使总空闲空间充足,空闲 Region 过于分散也会导致分配失败。

G1 可以在标记后回收已经无用的大对象。部分满足条件的大对象还会在回收暂停中被提前回收,因此它们无需全部等到 Full GC。

增大 G1HeapRegionSize 会提高大对象阈值,让部分对象改走普通分配路径。代价是回收区域的粒度变粗。调整前应结合对象实际尺寸与日志中的 Humongous Region 数量,确认收益。

3. Remembered Set​

回收 Region A 时,G1 既要扫描 GC Roots,也要找到其他区域指向 A 的引用。否则,只扫描 A 内部就会漏掉仍被外部使用的对象。Remembered Set(RSet,记忆集)保存这类外部引用的摘要。

RSet 的方向可以理解为“谁从外面指向当前 Region”。引用更新时,G1 通过写屏障和卡表维护这些摘要。卡片对应一段堆地址,所以回收时还要扫描相关卡片,才能找到具体对象引用。这样就能省去对全部未回收 Region 的遍历。

这种局部扫描带来以下四项成本:

  • 引用写入需要执行额外的屏障逻辑。
  • 后台线程需要处理卡片更新。
  • RSet 本身需要占用内存。
  • 跨 Region 引用密集时,合并摘要与扫描卡片的工作量会增加。

4. Young GC​

Young GC 主要回收年轻代,让新对象继续获得分配空间。G1 会把年轻 Region 加入 collection set,也就是本次要回收的区域集合。应用线程暂停后,G1 把存活对象复制到 Survivor 或 Old。成功疏散后,原 Region 就能整体释放。

复制将存活对象集中到目标区域,同时完成空间整理,减少区域内碎片。这个过程需要足够的目标空间。余量不足时会发生 evacuation failure,即疏散失败。失败区域需要继续保留对象,并由后续回收处理。

CMS 正常周期采用标记清除,会留下空闲碎片,具体对比见Serial、Parallel 与 CMS。

5. 并发标记与 Mixed GC​

年轻代之外,G1 还要知道哪些老年代区域值得回收。达到启动条件后,G1 开始并发标记,统计各区域的存活对象。标记的大部分工作与应用并发执行,但周期中仍有 Remark 等停顿阶段。Remark 用于完成剩余标记工作,确保存活信息完整。

标记结束后,G1 从候选 Old Region 中选出一部分,和年轻代一起疏散,这就是 Mixed GC。选择时既看能回收多少空间,也看预计耗时能否放进暂停预算。

“Garbage First”强调优先回收收益较高的区域。候选范围、必需工作和暂停预测都会影响选择,所以每次选中的区域未必是垃圾比例最高的那个。

5.1 IHOP 与启动时机​

并发标记需要时间,启动太晚就会来不及腾出空间。IHOP 用于判断老年代占用何时达到启动并发标记的阈值。JDK 21 默认启用 G1UseAdaptiveIHOP,根据运行数据调整这个阈值。预测会结合标记耗时、老年代分配速率与预留空间。

InitiatingHeapOccupancyPercent=45 是默认初始阈值。收集到足够样本后,G1 会使用自适应计算结果。也就是说,45% 用于初始阶段,之后的启动时机可以变化。

若要手动设定阈值,需要关闭自适应:

-XX:-G1UseAdaptiveIHOP -XX:InitiatingHeapOccupancyPercent=30

这组参数仅展示手动模式。实际调整应以标记耗时和分配增长为依据。默认自适应仍应作为基线。

6. 暂停目标​

G1 用暂停目标控制每次安排多少回收工作。以下参数把目标设为 200ms:

java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar app.jar

G1 根据历史统计预测回收集合的成本,再安排本次工作。实际暂停还受以下几类因素影响:

  • 存活对象突然增多,需要复制更多数据。
  • RSet 与根扫描的工作量高于预测。
  • 大对象改变空间布局,增加回收压力。
  • 机器负载和系统调度延迟使 GC 线程难以及时执行。

因此,200ms 是尽量达到的软目标,实际暂停仍会超出它。

更低的目标会缩小单次工作预算。G1 可以缩小年轻代,但年轻代也会更快填满,从而增加回收频率。GC CPU 随之上升时,应用吞吐会受影响。调整前应先确认业务延迟由 GC 主导,再一起比较暂停、CPU 和吞吐。

7. G1 的 Full GC​

Full GC 是 G1 对全堆执行整理的回收路径,停顿代价较高。分配失败且正常回收无法及时腾出空间时,G1 会尝试这条路径。显式 GC 请求与诊断命令也能触发它。

先看日志中的 cause,也就是本次回收的触发原因。若原因是空间不足,需要检查以下五项因素:

  • 并发标记是否启动太晚,导致 Mixed GC 来不及回收老年代。
  • live set 是否接近最大堆。live set 指回收后仍需保留的存活对象总量。
  • 分配或晋升速率是否突然升高。
  • Humongous 对象是否缺少连续 Region。
  • 疏散时是否缺少存放复制对象的目标空间。

处理时先减少无界保留与异常分配,再评估堆余量和并发周期是否及时完成。需要调整 IHOP 等参数时,一次只改少量相关参数,便于判断效果。

8. G1 日志​

开启统一日志,观察 Region 与回收集合:

java -Xlog:gc*,gc+heap=debug:file=gc.log:time,uptime,level,tags -jar app.jar

分析时,把以下五组信息放到同一时间线上:

  1. 确认 Young、Mixed、Concurrent Cycle 或 Full 的事件类型与触发原因。
  2. 比较 Eden、Survivor、Old 和 Humongous Region 的数量变化。
  3. 查看 Evacuate、Merge/Scan Heap Roots 和 Reference Processing 等阶段耗时。它们分别涉及疏散、合并或扫描堆根,以及特殊引用处理。
  4. 对照暂停前后的堆占用,计算此次回收腾出了多少空间。
  5. 关联同期分配率、请求延迟和 CPU,判断回收对业务的影响。

例如,复制阶段慢时要看存活量,扫描阶段慢时要看跨区域引用。单独一行暂停时长不足以决定扩大堆还是减少引用扫描。

跨事件分析见GC 停顿分析。

9. 常见问题​

9.1 G1 还分年轻代和老年代吗?​

G1 保留年轻代和老年代,只是各代由一组离散 Region 组成。Region 的角色可以重新分配,所以各代容量也可以动态变化。

9.2 RSet 保存的是完整对象图吗?​

9.3 Mixed GC 和 Full GC 有什么区别?​

10 · 面试题卡片​

10.1 G1 怎样通过 Region 和 RSet 完成局部回收​

出现公司:阿里巴巴、某云原生企业

考察重点:理解 RSet、回收集合与疏散复制的配合。

相关内容:Region至并发标记与 Mixed GC。

参考回答

G1 选择一组 Region 作为回收集合,再用 RSet 找到集合外指向集合内的引用。

暂停时,G1 扫描 GC Roots 和相关卡片,把存活对象复制到其他 Region。成功疏散后,原区域就能整体释放。

这种方式用写屏障、后台卡片处理和 RSet 内存,换取较小的扫描范围。跨 Region 引用越密集,合并与扫描摘要的成本也越高。

10.2 G1 的暂停目标为什么不是保证​

出现公司:阿里云

考察重点:理解暂停预测的依据、偏差与吞吐代价。

相关内容:暂停目标至G1 日志。

参考回答

MaxGCPauseMillis 是软目标,因为 G1 按历史数据预测耗时,实际工作量仍会变化。

比如,存活量、RSet 扫描或大对象压力突然增加,系统调度也会延迟 GC 线程执行。

目标过低会让 G1 缩小年轻代,并更频繁地收集。这样虽能减少单次工作量,却会增加 GC CPU、损失吞吐。因此,要先定位超时阶段,再用相同负载比较端到端延迟。

10.3 G1 总在堆占用达到 45% 时启动并发标记吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:说明初始 IHOP 怎样被自适应阈值替代,以及手动固定阈值需要哪些参数。

相关内容:IHOP 与启动时机。

参考回答

45% 是默认初始阈值,G1 收集到足够样本后会自适应调整它。

预测结合标记耗时、老年代分配速率与预留空间,目的是及时启动标记。

手动固定阈值时,需要用 -XX:-G1UseAdaptiveIHOP 关闭自适应。例如,再配合 -XX:InitiatingHeapOccupancyPercent=30,才是固定为 30% 的模式。

10.4 G1 总空闲空间充足,为何大对象仍会分配失败?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释分散的空闲 Region 为什么无法满足大对象分配,以及尾部浪费怎样影响可用空间。

相关内容:Humongous 对象。

参考回答

Humongous 对象需要连续 Region,因此空闲区域过于分散时,总量足够也会分配失败。

此外,对象末尾 Region 的剩余空间要等对象回收后才能复用。分析时应同时检查对象实际尺寸、所需 Region 数量与连续空间。

11. 参考资料​