← 返回题目列表

G1 垃圾收集器的原理是什么?为什么能做到可预测停顿?

高频 困难 第 14 / 34 题 更新于 2026/07/26
G1Region记忆集Mixed GC

简化版

G1(Garbage First)是 JDK 9 起的默认收集器,核心思想是把堆划分成很多大小相等的 Region(区),每个 Region 动态扮演 Eden/Survivor/Old/Humongous 角色,不再是固定连续的分代。回收时 G1 优先回收「垃圾最多、回收收益最高」的 Region(Garbage First 得名),并用一个可预测停顿模型:你设定目标停顿时间(如 -XX:MaxGCPauseMillis=200),G1 就估算每个 Region 的回收成本,只挑「在预算内能回收的 Region」,从而把停顿控制在目标附近。它兼顾大堆和低停顿,用「化整为零 + 挑肥拣瘦」取代了 CMS。

详细版

核心结构:Region 化的堆

传统分代:            G1 的 Region 化:
[   Eden   |S|S|      [E][O][E][H][ ][O][S][E][O][ ]...
    Old        ]      每个 Region 大小相等(1~32MB),
连续、固定边界        角色动态分配,不要求连续
  • Region:堆被切成 1000+ 个大小相等的小块,每块可以是 Eden、Survivor、Old 或 Humongous(存放大对象,≥Region 一半)。
  • 逻辑分代:仍有年轻代/老年代概念,但它们是「一组 Region 的集合」,物理上不连续、可动态调整数量。

两种回收

类型回收范围触发
Young GC所有年轻代 Region(Eden+Survivor)Eden 满
Mixed GC所有年轻代 + 部分收益高的老年代 Region老年代占比达阈值(-XX:InitiatingHeapOccupancyPercent,默认 45%)触发并发标记后

为什么能可预测停顿:G1 维护每个 Region 的「回收成本 vs 可释放空间」统计,回收时按目标停顿时间 MaxGCPauseMillis 挑选一批「性价比最高」的 Region 组成回收集(Collection Set, CSet),只回收这批,从而把停顿控制在预算内。

跨代引用靠记忆集(RSet):每个 Region 有一个 RSet,记录「哪些其他 Region 引用了我」,这样回收某个 Region 时不用扫全堆找引用,只查它的 RSet 即可。

⚠️ Humongous(大对象,≥Region 一半大小)会直接分配到连续的 Humongous Region,绕过 Eden。大对象过多会导致 Humongous Region 碎片和频繁 GC——所以 G1 下要留意「巨型对象」问题,必要时调大 Region 大小(-XX:G1HeapRegionSize)。

完整版教学

一、G1 要解决什么:CMS 的两大痛点

要理解 G1 的设计,先看它取代的 CMS 有什么问题:

CMS 痛点1:内存碎片
  CMS 用「标记-清除」,回收后老年代留下大量不连续空隙
  → 大对象找不到连续空间 → 被迫触发 Full GC(还是 STW 的单线程整理)

CMS 痛点2:停顿不可控
  堆越大,标记/清理时间越长,停顿无法预估
  用户无法说"我要停顿不超过 200ms"

G1 的两个核心创新正是针对这两点:① Region + 复制整理——回收时把存活对象复制到别的 Region,天然整理内存、不产生碎片;② 可预测停顿模型——让用户设定目标停顿,G1 自己控制每次回收多少。理解「G1 是为了大堆下的可控停顿 + 无碎片」而生,后面的机制都是服务这个目标。

二、Region:化整为零的堆

G1 把堆切成 1000 个左右大小相等的 Region(1~32MB,-XX:G1HeapRegionSize 或按堆大小自动定)。这是 G1 一切能力的基础:

堆 = 一大堆 Region,每个 Region 角色动态:
[E][E][S][O][O][H][ ][E][O][ ][S][O]...
 E=Eden  S=Survivor  O=Old  H=Humongous  空=Free

好处:
- 回收单位是 Region(小),可以只回收一部分,不必整堆一起
- 存活对象复制到空 Region,回收完的 Region 整个变 Free → 无碎片
- 年轻代/老年代只是"Region 的分类标签",数量可动态增减

关键转变:传统 GC 的回收单位是「整个年轻代」或「整个老年代」,G1 的回收单位是「一批精选的 Region」。这让「部分回收、增量推进、按预算停顿」成为可能——这是 G1 区别于所有前辈的根本。

三、可预测停顿模型:Garbage First 的精髓

G1 名字里的「Garbage First」就是它的调度策略:优先回收垃圾最多的 Region,因为回收它们性价比最高(花同样的复制成本,释放更多空间)。

G1 为每个 Region 维护统计:预计回收耗时、可释放空间
用户设定:-XX:MaxGCPauseMillis=200(目标停顿 200ms)

回收时 G1 做一道"背包问题":
  在 200ms 预算内,挑选哪些 Region 组成回收集(CSet) 能释放最多空间?
  → 按"垃圾多、回收快"排序,贪心选够 200ms 的量就停
  → 只回收这批 CSet,停顿≈200ms

用一个数字例子:假设 Region A 有 90% 垃圾、Region B 有 30% 垃圾,回收成本相近,G1 优先选 A(释放更多)。这套「估算成本 + 按预算挑选 + 优先高收益」的机制,就是「可预测停顿」的实现原理。注意 MaxGCPauseMillis目标不是保证——设太小(如 10ms)会导致每次回收的 Region 太少、GC 太频繁,反而降低吞吐。

四、记忆集 RSet 与卡表:解决跨代/跨 Region 引用

G1 只回收部分 Region 时遇到一个难题:要判断 CSet 里的对象是否存活,得知道「有没有 CSet 外的对象引用它」。如果每次都扫全堆找引用,代价巨大。解法是记忆集(Remembered Set, RSet)

每个 Region 有一个 RSet,记录"谁引用了我"(指向我的跨 Region 引用来源)
回收 Region X 时:
  不用扫全堆,只需查 X 的 RSet → 就知道哪些外部 Region 引用了 X 的对象
  → 把这些外部引用也当作 GC Roots 的一部分,判断 X 内对象存活

维护 RSet 靠写屏障 + 卡表(Card Table):
  引用赋值时(写屏障),若是跨 Region 引用,
  把来源 Region 对应的"卡(Card, 512字节一块)"标脏,
  异步更新到目标 Region 的 RSet

RSet 是 G1 能「只回收部分 Region」的关键——用空间(每个 Region 存 RSet,约占堆的 1%~20%)换「不用全堆扫描」的时间。这也是 G1 内存开销比 CMS 大的原因之一。面试深挖「G1 怎么处理跨代引用」,答案就是 RSet + 卡表 + 写屏障。

五、完整回收流程:Young GC 与 Mixed GC

G1 的回收分两大类,配合一个并发标记周期:

① Young GC(STW,但快):
   Eden 满 → 回收所有年轻代 Region → 存活对象复制到 Survivor/Old Region

② 并发标记周期(老年代占用达 IHOP 阈值 45% 触发):
   初始标记(STW,搭 Young GC) → 并发标记(与业务并发,三色标记+SATB)
   → 最终标记(STW,处理 SATB 队列) → 清理(STW,统计各 Region 存活量、回收全空 Region)

③ Mixed GC(STW):
   并发标记后,接下来几次回收变成 Mixed —
   回收全部年轻代 + 一部分"垃圾最多"的老年代 Region(按停顿预算挑)
   → 分多次 Mixed GC 逐步清理老年代,每次只清一部分,控制停顿

关键点:G1 不做(也尽量避免)传统的单次 Full GC——它把老年代的清理拆成多次 Mixed GC,每次只处理一批 Region,避免「一次性整理整个老年代」的长停顿。只有当 Mixed GC 也跟不上分配速度、空间耗尽时,才退化成 Full GC(G1 的 Full GC 在 JDK 10 前是单线程、很慢,是要极力避免的信号)。

六、G1 vs CMS:选型与调优

维度CMSG1
堆结构传统连续分代Region 化
回收算法标记-清除(有碎片)复制整理(无碎片)
停顿控制不可预测可设目标停顿
跨代引用卡表RSet + 卡表
适用堆中小堆(<4~6GB)大堆(>6GB)
JDK已废弃(JDK 14 移除)JDK 9+ 默认

调优要点:① 别把 MaxGCPauseMillis 设太小(太小 → GC 太频繁 → 吞吐降),一般 200ms 起步;② 留意 Humongous 大对象(≥Region 一半直接进 Humongous Region,多了会碎片化,可调大 Region);③ 关注 Full GC(G1 出现 Full GC 说明并发回收跟不上,要调 IHOP 阈值提前触发并发标记,或加堆)。

记忆钩子:「G1 = 堆切 Region(角色动态、复制整理无碎片)+ Garbage First(优先回收垃圾多的 Region)+ 可预测停顿(按 MaxGCPauseMillis 挑 CSet)+ RSet 解决跨 Region 引用;老年代拆成多次 Mixed GC 清理,极力避免 Full GC」

七、常见误区与追问

  • 误区:G1 没有分代了。 仍有年轻代/老年代概念,只是它们是「一组 Region 的逻辑集合」,物理不连续、数量可动态调整。
  • 误区:MaxGCPauseMillis 设越小停顿越小越好。 设太小会导致每次回收 Region 太少、GC 过于频繁,吞吐骤降;它是目标不是硬保证,一般 200ms 起。
  • 误区:G1 完全没有 STW。 Young GC、初始/最终标记、Mixed GC 都有 STW,只是通过「回收部分 Region + 可控 CSet」把停顿压短;并发标记阶段才与业务并发。
  • 误区:G1 永远不会 Full GC。 当 Mixed GC 跟不上分配、空间耗尽时会退化为 Full GC(早期单线程很慢),这是需要调优的危险信号。
  • 追问:G1 怎么处理跨 Region 引用? 每个 Region 维护 RSet 记录「谁引用我」,回收时查 RSet 而非扫全堆;RSet 靠写屏障 + 卡表异步维护。
  • 追问:Humongous 对象是什么,有什么影响? 大小 ≥Region 一半的大对象,直接分配到连续的 Humongous Region、绕过 Eden;过多会碎片化并触发频繁 GC,可调大 G1HeapRegionSize。
  • 追问:G1 和 CMS 的并发标记有什么不同? 都用三色标记,但 CMS 用增量更新写屏障、G1 用 SATB;G1 的 Region 结构让它能按停顿预算增量回收老年代(Mixed GC),CMS 只能整块清理老年代。

八、加强记忆

G1 是为「大堆下的可控停顿 + 无碎片」而生,取代 CMS。核心是把堆切成上千个大小相等的 Region,每个 Region 角色(Eden/Survivor/Old/Humongous)动态分配、物理不连续——回收单位从「整个分代」变成「一批精选 Region」,且靠复制整理天然消除碎片。它的名字「Garbage First」点明策略:优先回收垃圾最多、性价比最高的 Region,配合可预测停顿模型——用户设 MaxGCPauseMillis,G1 估算每个 Region 回收成本,按预算挑选组成回收集(CSet),把停顿控制在目标附近(是目标不是保证,设太小反伤吞吐)。跨 Region 引用靠RSet(记忆集)+ 卡表 + 写屏障解决,回收某 Region 只查它的 RSet 而非扫全堆(代价是 RSet 占额外内存)。老年代清理拆成多次 Mixed GC(并发标记达 45% IHOP 触发后),每次只清一批 Region,极力避免单次 Full GC。一句话「Region 化堆、复制无碎片、垃圾优先、按预算挑 CSet 控停顿、RSet 管跨代、Mixed GC 增量清老年代」。