JVM / GC 深入 —— 从三色标记到 GC 排查实战
10 题 · 平均 3/5 · low 2.67 · medium 3.5 · high 2.67 · 第 1 轮
**核心考点定位**:考察三色标记的**严格定义**+**并发场景的内存安全问题**+**修复机制(写屏障/读屏障)**。
**知识点拆解**:
1. **三色严格定义(HotSpot 采用)**
- **白色(White)**:**未被 GC 访问过**,可能是垃圾也可能是存活(初始状态)
- **灰色(Gray)**:已被访问,但**引用的对象还有未访问**(工作队列 / 待扫描)
- **黑色(Black)**:已访问,且**所有引用都已扫描完毕**(确定存活)
2. **算法流程**
```
1. 所有对象初始为白色
2. GC Roots 全部染黑
3. 取一个黑节点,扫描其引用:
- 引用的对象若为白 → 染灰、加入工作队列
- 该黑节点所有引用扫描完 → 完成
4. 从工作队列取灰节点:
- 扫描其引用,白→灰、入队
- 所有引用扫描完 → 染黑
5. 工作队列为空时结束
6. 剩余白色对象 = 垃圾,回收
```
**不变式(正确性条件)**:
- **规则 1**:黑不能指白(否则漏回收)
- **规则 2**:灰不能指黑(否则可能误回收,因为灰节点扫描时看不到已染黑的子节点)
3. **并发场景的两大问题(重点!)**
三色标记的核心动机是**并发标记**——让 GC 与用户线程并发扫描,减少 STW。但并发带来两个引用变动:
- **对象消失(漏回收)**:黑节点 X 原本引用白对象 Y,扫描后删链;紧接着用户线程创建黑节点 Z 引用 Y;此时 Y 是白,被误回收 → **Y 活着但被回收**
- **对象复活(误回收)**:黑节点 X 引用白对象 Y 时,用户线程先创建白节点 Z 引用 X,扫描完 Y 后删掉 X→Y 链;此时 Y 被误认为白垃圾,但通过 Z→X→? 关系其实……(更常见是**误回收**:黑节点被引用后仍被错认为垃圾)
**准确表述**:
- **黑指白(Broken by Black→White add)**:漏回收
- **白指黑(Broken by White→Black add)**:误回收
4. **两种修复方案**
| 修复方案 | 屏障类型 | 使用者 | 核心思想 |
|---------|---------|-------|---------|
| **写屏障** | Write Barrier | CMS、G1、ZGC(经典) | 修复"黑指白":**对象引用变白 → 父对象重染灰** |
| **读屏障** | Read/Load Barrier | Shenandoah、ZGC(现代) | 修复"白指黑":**读指针时检查有效性,无效则重新标记** |
5. **ZGC 的启示**
- **多映射(Multi-Mapping)**:同一虚拟地址映射多个物理地址,移动对象时改映射而非引用
- **读屏障**:读取指针时检查指针有效性(Validated Address)
- **STW 只有两瞬间**:Initial Mark 和 Final Relocate,都 <1ms
- 三色标记 + 读屏障 = ZGC 的核心技术组合
6. **CMS 的具体流程中的三色标记**
- Initial Mark(STW):把 GC Roots 染黑
- Concurrent Mark:三色并发扫描(灰→黑、白→灰)
- Remark(STW):处理并发期间的引用变动,靠写屏障让变动可追踪
**考察方向**:
- **深度**:三色定义准确表述、内存安全问题的严格推理、写屏障具体规则
- **广度**:写屏障 vs 读屏障的取舍、ZGC/Shenandoah 的现代方案
- **应用**:Remark 如何缩短、CMS/G1/ZGC 的三色标记实现差异
**延伸递进面试题**:
1. **三色标记的"对象消失"问题具体怎么走通?**
- 解答要点:黑节点 X 引用白节点 Y → 扫描 X 完成后断链 X→Y → 用户线程创建黑节点 Z 引用 Y → Y 变灰再变黑 → 结束遍历 Y 是黑(正常);但如果 Y 从未被访问,Y 保持白 → 被误回收
2. **写屏障具体怎么处理?**
- 解答要点:写屏障在对象引用赋值时插入的检查:如果新引用指向白对象,把父对象重染灰;HotSpot 中通过 `-XX:+CMSClassUnloadingEnabled` 相关逻辑实现
3. **为什么 ZGC 从写屏障转向读屏障?**
- 解答要点:写屏障频繁触发(每次赋值)影响吞吐;读屏障只在读取指针时检查,对代码侵入小;配合多映射实现"移动对象不改引用"
4. **Shenandoah 的 Brooks 转发是什么?**
- 解答要点:对象头存转发指针,读屏障检查指针有效性时顺带完成对象移动;STW <10ms
5. **三色标记和可达性分析的关系?**
- 解答要点:三色标记是可达性分析的**并发实现**,本质都是从 GC Roots 遍历;区别在是否并发、如何处理引用变动
面试官您好,**三色标记算法**是并发 GC 的核心算法,让 GC 扫描和用户线程并发运行,减少 STW。
**三色严格定义**(HotSpot 采用):
- **白色**:从未被 GC 访问过,可能是垃圾也可能是存活(**初始状态**)
- **灰色**:已被访问,但**引用的对象还有未访问的**(工作队列,待扫描)
- **黑色**:已访问,且**所有引用都扫描完毕**(确定存活)
**算法流程**:GC Roots 全部染黑 → 取灰节点扫描子节点,白节点变灰入队,灰节点扫描完变黑 → 工作队列为空时结束 → **剩余白色对象就是垃圾,回收**。
**核心思想**:白色 = 潜在垃圾,黑色 = 确定存活,灰色 = 中间态。
**并发带来的两个内存安全问题**:
1. **对象消失(漏回收)**:黑节点 X 引用白节点 Y → 扫描完成断链 X→Y → 用户线程创建黑节点 Z 引用 Y → 此时 Y 从未被访问保持白,被误回收。这违反"黑不能指白"规则。
2. **对象复活(误回收)**:白节点变黑后仍被断链,导致实际存活的对象被标成白。这违反"灰不能指黑"规则。
**两种修复方案**:
- **写屏障(Write Barrier)**:修复"黑指白"问题。规则是**当对象引用被赋值时,如果新引用指向白对象,把父对象重染灰**。使用者:CMS、G1、ZGC 经典模式。
- **读屏障(Read/Load Barrier)**:修复"白指黑"问题。规则是**读指针时检查有效性,无效则重新标记为黑**。使用者:Shenandoah、ZGC 现代模式。
**为什么 ZGC 从写屏障转向读屏障?** 写屏障每次赋值都触发,影响吞吐;读屏障只在读取指针时检查,对代码侵入小。ZGC 配合**多映射**(同一虚拟地址映射多物理地址,移动对象改映射而非改引用)实现 STW <1ms。
**CMS 中的三色标记**:Initial Mark(STW)染黑 GC Roots → Concurrent Mark 并发三色扫描 → Remark(STW)靠写屏障追踪并发期间的引用变动。
一句话总结:**三色标记是可达性分析的并发实现**——本质都是从 GC Roots 遍历,但通过三色状态管理并发下的引用变动,靠写屏障或读屏障保证内存安全。
---
**核心考点定位**:考察对 YoungGC 和 FullGC 触发机制的完整理解,以及 FGC 的避免策略。
**知识点拆解**:
1. **YoungGC 触发条件**:Eden 区满(默认,最常见)、大对象直接在老年代分配导致老年代空间不足触发 FGC 前先 YoungGC、手动 System.gc() 建议回收新生代
2. **FullGC 完整触发条件(6 种)**:老年代空间不足(对象晋升、大对象直接分配)、Survivor 空间不足(空间分配担保失败)、Metaspace 不足、System.gc() 显式调用、-XX:+ExplicitGCInvokesConcurrent 触发并发 Full GC、G1 的 Mixed GC 跟不上回收速度退化到 Full GC
3. **FGC 的避免策略**:增大老年代、调整晋升阈值(MaxTenuringThreshold)、用 G1 Mixed GC 替代 Full GC、避免大对象直接进入老年代
**考察方向**:
- **深度**:6 种 FGC 触发条件的严格区分
- **广度**:与 G1 Mixed GC、ZGC 不分代的关系
- **应用**:FGC 排查和避免策略
**延伸递进面试题**:
1. **G1 的 Mixed GC 和 Full GC 有什么区别?**
- 解答要点:Mixed GC 是 Young GC + 部分老年代 Region,Full GC 是整代回收;Mixed GC 可预测停顿,Full GC 是最后手段
2. **如何避免 FGC?**
- 解答要点:增大老年代、调整晋升阈值、G1 Mixed GC 替代、避免大对象直接老年代、排查内存泄漏
3. **System.gc() 有什么影响?**
- 解答要点:建议回收(不保证立即执行),JDK 9+ 默认关闭(-XX:+ExplicitGCInvokesConcurrent),生产环境应禁用
**【一句话结论】**
面试官您好,YoungGC 是 Eden 满触发,FullGC 有 6 种触发条件,核心是老年代空间不足或 Metaspace 不足。
**【三点展开】**
第一,YoungGC 最常见,Eden 区满就触发,回收新生代,存活对象复制到 Survivor。频率高但速度快,通常 10-100ms。
第二,FullGC 有 6 种触发条件:老年代空间不足(对象晋升或大对象直接分配)、Survivor 空间不足(空间分配担保失败)、Metaspace 不足、System.gc() 显式调用、-XX:+ExplicitGCInvokesConcurrent 触发并发 Full GC、G1 的 Mixed GC 跟不上回收速度退化到 Full GC。其中老年代空间不足是最常见的。
第三,FGC 的避免策略有 3 种:增大老年代空间、调整晋升阈值(MaxTenuringThreshold)让对象更早晋升或更晚晋升、用 G1 的 Mixed GC 替代 Full GC(Mixed GC 是 Young GC + 部分老年代 Region,可预测停顿)。另外要排查内存泄漏,避免对象持续增长导致老年代满。
**【一句边界】**
当然,System.gc() 在 JDK 9+ 默认关闭,生产环境应禁用(-XX:+ExplicitGCInvokesConcurrent 改为 -XX:-ExplicitGCInvokesConcurrent)。FGC 是最后手段,频繁触发说明有内存泄漏或参数配置不当。
**核心考点定位**:考察对跨代引用的严格定义、问题本质和解决机制的理解。
**知识点拆解**:
1. **跨代引用的严格定义**:特指**老年代对象引用新生代对象**(Old→Young,不是双向),不是"年轻代引用老年代"
2. **问题本质**:FullGC 时若从老年代 GC Roots 遍历新生代,需要扫描整个新生代,O(n) 爆炸(新生代通常 1/3 堆大小,扫描成本高)
3. **解决机制**:**Remembered Set**(G1,每个老年代 Region 维护跨到年轻代的引用集合)+ **Card Table**(CMS/Parallel,每 512 字节标记一个 Card,写屏障时更新 Card 状态)+ **写屏障**(对象引用赋值时的检查代码,让 GC 高效处理并发修改)
**考察方向**:
- **深度**:跨代引用的方向性(Old→Young)和 Remembered Set/Card Table 机制
- **广度**:与写屏障、三色标记的关系
- **应用**:G1 和 CMS 的实现差异
**延伸递进面试题**:
1. **Remembered Set 和 Card Table 有什么区别?**
- 解答要点:Remembered Set 是 G1 的跨 Region 引用集合,Card Table 是 CMS/Parallel 的每 512 字节标记 Card
2. **写屏障在跨代引用中起什么作用?**
- 解答要点:对象引用赋值时插入的检查代码,更新 Card Table 或 Remembered Set,让 GC 只处理"脏"的引用
3. **为什么跨代引用不是双向的?**
- 解答要点:Young→Old 引用是正常的(新生代对象引用老年代对象,FullGC 时会扫描),问题只在 Old→Young(FullGC 时从老年代 GC Roots 遍历新生代会 O(n) 爆炸)
**【一句话结论】**
面试官您好,跨代引用特指老年代对象引用新生代对象,FullGC 时若全堆遍历会 O(n) 爆炸,通过 Remembered Set/Card Table + 写屏障解决。
**【三点展开】**
第一,跨代引用是单向的,特指 Old→Young 引用,不是双向的。Young→Old 引用是正常的,FullGC 时会扫描。问题只在 Old→Young:FullGC 时若从老年代 GC Roots 遍历新生代,需要扫描整个新生代,O(n) 爆炸(新生代通常 1/3 堆大小)。
第二,解决机制有两种:G1 用 Remembered Set,每个老年代 Region 维护一个跨到年轻代的引用集合,FullGC 时只扫描这些集合里的引用,避免全堆遍历。CMS/Parallel 用 Card Table,每 512 字节标记一个 Card,写屏障在对象引用赋值时更新 Card 状态,FullGC 时只扫描"脏"的 Card。
第三,写屏障是关键:对象引用赋值时插入的检查代码,如果新引用指向新生代对象,更新 Card Table 或 Remembered Set。这样 GC 可以高效处理并发修改,避免全堆扫描。
**【一句边界】**
当然,跨代引用不是双向的。Young→Old 引用是正常的,FullGC 时会扫描。问题只在 Old→Young,因为从老年代 GC Roots 遍历新生代会 O(n) 爆炸。另外,G1 的 Region 设计让跨代引用问题更容易管理(Remembered Set 按 Region 维护)。
**核心考点定位**:考察分代机制的结构性理解(Eden/Survivor/Old 具体划分)+ 分代设计的动机(弱分代假说)+ 晋升机制细节。
**知识点拆解**:
1. **堆的完整结构(HotSpot 默认)**
```
Heap
├── Young Generation (约 1/3)
│ ├── Eden (约 80%) # 新对象首次分配在这里
│ ├── From Survivor (约 10%) # 上一次存活对象
│ └── To Survivor (约 10%) # 本次存活对象目标
└── Old Generation (约 2/3) # 长寿对象 + 大对象
```
- **Eden:From:To = 8:1:1** 是 HotSpot 优化:Survivor 只保留约 10%,让老年代吃满剩余空间
- JDK 8 之后:MetaSpace 独立于堆,永久代→元空间
2. **为什么分代?——弱分代假说**
- **弱分代假说**:几乎所有对象朝生夕死
- **强分代假说**:熬过几次 GC 的对象更难死
- **推论**:新生代存活少 → 用**复制算法**(无碎片、快);老年代存活多 → 用**标记-整理**(无碎片、空间利用率高)
- **反证**:如果堆不分代,全堆统一用标记-清除,会有碎片;用复制算法,空间浪费且大对象无法处理;用标记-整理,每次 FullGC 都要移动大部分对象,停顿爆炸
- 所以分代本质是**用空间的复杂性,换取算法最优化的可能性**
3. **对象晋升的 4 条规则**(HotSpot 实际实现)
- **年龄晋升**:对象每经历一次 Young GC 年龄 +1,超过 `-XX:MaxTenuringThreshold`(默认 15,动态计算)就晋升
- **动态年龄判定**:Survivor 中同龄对象总和 > Survivor 50%,该年龄及更大的全部晋升(避免"熬到老")
- **大对象晋升**:超过 `-XX:PretenureSizeThreshold` 直接在老年代分配(大对象在 Eden 复制开销大)
- **空间分配担保**:Survivor 装不下就把剩余对象搬到老年代
4. **两类 GC 的边界**
- **Young GC**:只回收新生代,Survivor 之间来回复制
- **Mixed GC**(G1 特有):Young GC + 部分老年代 Region(按"脏度"排序)
- **Full GC**:整代回收(新生代 + 老年代 + Metaspace),STW 长,是最后手段
- 老年代触发:老年代空间不足、Survivor 晋升失败、Metaspace 不足、System.gc() 显式调用
5. **分代的代价:跨代引用**
- 老年代对象引用新生代对象,FullGC 时若从老年代根遍历新生代会 O(n)
- **Remembered Set**:每个老年代 Region 维护一个跨到年轻代的引用集合(G1)
- **Card Table**:每 512 字节标记一个 Card,写屏障时更新 Card 状态(CMS/Parallel)
- 写屏障(Write Barrier):对象引用赋值时插入的检查代码,让 GC 高效处理并发修改
**考察方向**:
- **深度**:4 种晋升条件、写屏障/Remembered Set 机制、动态年龄判定
- **广度**:不同 GC 器(G1/ZGC/Shenandoah)是否分代、"不分代"的意义
- **应用**:`-Xmn` / `-XX:SurvivorRatio` / `-XX:MaxTenuringThreshold` 调优;晋升过早导致老年代撑爆
**延伸递进面试题**:
1. **为什么 Survivor 只有 10% 空间?**
- 解答要点:新生代存活率通常 < 10%,留 20%(From+To)已足够;剩余 80% 给 Eden 保证分配效率;Survivor 太小也能逼早熟对象晋升,避免"熬到老"
2. **MaxTenuringThreshold 是动态的吗?**
- 解答要点:是,HotSpot 会动态计算"能让 Survivor 里存活对象总年龄刚好填满 Survivor 空间"的阈值;也可 `-XX:MaxTenuringThreshold=15` 硬设
3. **G1 的 Region 和传统分代有什么区别?**
- 解答要点:G1 是**逻辑分代**(Region 可标记为 Young/Old/Humongous),不是物理分代;回收可以选任意 Region,通过 Remembered Set 处理跨代引用,避免全堆扫描
4. **跨代引用怎么处理?**
- 解答要点:Remembered Set(G1)或 Card Table(CMS/Parallel),配合写屏障在对象引用赋值时更新;避免 Full GC 时全堆遍历
5. **JDK 21+ 的分代 ZGC 是什么变化?**
- 解答要点:JDK 21 之前 ZGC 是"不分代"的(全堆统一),JDK 21 引入分代 ZGC(Young/Old 分区),进一步降低停顿、提高吞吐
面试官您好,Java 堆的分代核心是**为了不同对象用不同算法**,这是 GC 设计的核心权衡。
先说**为什么分代**,核心是**弱分代假说**——几乎所有对象朝生夕死,熬过几次 GC 的对象更难死。如果堆不分代:用复制算法浪费空间且大对象无法处理,用标记-整理每次 FullGC 都要移动大部分对象停顿爆炸。所以分代本质是**用空间复杂性换算法最优化的可能性**。
再说**如何分代**,HotSpot 堆分为新生代和老年代两部分。
**新生代占约 1/3**,内部又分 Eden、From Survivor、To Survivor 三区,比例是 **Eden:From:To = 8:1:1**。新对象默认先在 Eden 分配,Eden 满触发 Young GC,存活对象复制到 To Survivor,Survivor 之间来回交替(Eden:80%、两个 Survivor 各 10%——Survivor 只留 10% 是为了逼早熟对象晋升,避免"熬到老")。
**老年代占约 2/3**,存长寿对象和大对象。JDK 8 之后还有独立的 Metaspace(不在堆上)。
**对象晋升老年代的 4 条规则**:
1. **年龄晋升**:GC 次数达 `MaxTenuringThreshold`(默认 15,动态计算)
2. **动态年龄判定**:Survivor 中同龄对象总和 > 50%,该年龄及更大全部晋升
3. **大对象晋升**:超 `PretenureSizeThreshold` 直接在老年代分配,避免 Eden 复制开销
4. **空间分配担保**:Survivor 装不下就搬到老年代
**两类 GC 的边界**:**Young GC** 只回收新生代;**Mixed GC**(G1 特有)是 Young GC + 部分老年代 Region(按"脏度"排序);**Full GC** 是整代回收,是最后手段。所以说"老年代只有 Full GC 才动"是不准确的——G1 的 Mixed GC 也会回收部分老年代 Region。
**分代的代价是跨代引用问题**:老年代对象引用新生代对象时,Full GC 若全堆遍历会 O(n) 爆炸。所以引入了 **Remembered Set**(G1)或 **Card Table**(CMS/Parallel)+ **写屏障**,让 GC 只处理"脏"的引用,避免全堆扫描。
最后补充一点演进:**ZGC 早期是不分代的**(全堆统一),JDK 21 引入**分代 ZGC**,进一步降低停顿、提高吞吐。
---
**核心考点定位**:考察对 CMS 四阶段流程、STW 分布、局限性(碎片化/并发问题/退化)的完整理解。
**知识点拆解**:
1. **CMS 四阶段流程**:
- **初始标记(STW)**:标记 GC Roots 直接关联的对象,短暂停顿
- **并发标记(并发)**:从 GC Roots 遍历整个对象图,标记存活对象,与用户线程并发
- **重新标记(STW)**:处理并发标记期间的引用变动,通过写屏障追踪变动对象,短暂停顿
- **并发清理(并发)**:清理未被标记的对象,与用户线程并发
2. **CMS 的 STW 分布**:初始标记 + 重新标记 两次 STW,总停顿通常 < 50ms
3. **CMS 的局限性**:
- **并发导致引用变动**:并发标记期间用户线程修改引用,需要写屏障追踪
- **标记-清除产生碎片**:不移动对象,碎片化导致大对象无法分配,触发 FullGC
- **退化到 Serial Old**:并发回收失败(碎片化或回收速度跟不上分配速度),退化到单线程 FullGC
- **CPU 占用高**:并发标记需要多个线程,默认 (Runtime.getRuntime().availableProcessors() + 3) / 4
4. **CMS 的触发条件**:老年代使用率达阈值(默认 70%,-XX:CMSInitiatingOccupancyFraction)
5. **CMS 的写屏障**:对象引用赋值时插入的检查代码,把被修改对象重新标记为"脏",重新标记阶段扫描这些"脏"对象
6. **GC 器选型**:8-32G 用 G1,> 32G 用 ZGC/Shenandoah,CMS 适合 ≤ 8G、停顿敏感场景
**考察方向**:
- **深度**:四阶段 STW 分布、写屏障机制、碎片化导致退化
- **广度**:与 G1、Parallel Old 的对比
- **应用**:CMS 参数调优(-XX:+UseConcMarkSweepGC、-XX:CMSInitiatingOccupancyFraction)
**延伸递进面试题**:
1. **CMS 和 G1 有什么区别?**
- 解答要点:CMS 是并发标记-清除(有碎片),G1 是 Region 化 + Mixed GC(可预测停顿);CMS 适合 ≤ 8G,G1 适合 8-32G
2. **CMS 的碎片化问题如何导致 FullGC?**
- 解答要点:标记-清除不移动对象,碎片化导致大对象无法分配(空间分配担保失败),触发 FullGC;可通过 -XX:+UseCMSCompactAtFullCollection 触发碎片整理
3. **为什么 CMS 在 JDK 14 移除?**
- 解答要点:CMS 并发回收失败频繁退化到 FullGC、碎片化问题、维护成本高,G1/ZGC 更优
**【一句话结论】**
面试官您好,CMS 是并发标记-清除,四阶段(初始标记 STW → 并发标记 → 重新标记 STW → 并发清理),适合 ≤ 8G、停顿敏感场景,JDK 14 已移除。
**【三点展开】**
第一,CMS 四阶段:初始标记(STW,标记 GC Roots 直接关联对象)、并发标记(并发遍历对象图,标记存活对象)、重新标记(STW,处理并发期间的引用变动,通过写屏障追踪)、并发清理(清理未标记对象)。两次 STW 总停顿通常 < 50ms。
第二,CMS 的局限性:并发导致引用变动需要写屏障追踪、标记-清除不移动对象产生碎片导致大对象无法分配触发 FullGC、并发回收失败退化到 Serial Old(单线程 FullGC)、CPU 占用高(并发标记需要多个线程)。
第三,CMS 的触发条件和参数:老年代使用率达阈值(默认 70%,-XX:CMSInitiatingOccupancyFraction)触发。参数 -XX:+UseConcMarkSweepGC 启用,-XX:+UseCMSCompactAtFullCollection 触发碎片整理。JDK 9 废弃,JDK 14 移除,推荐 G1(8-32G)或 ZGC(> 32G)。
**【一句边界】**
当然,CMS 适合堆 ≤ 8G、停顿敏感的场景。堆 > 8G 推荐 G1(Region 化 + Mixed GC 可预测停顿),堆 > 32G 推荐 ZGC(STW < 1ms)。CMS 已移除,新项目不应使用。
**核心考点定位**:考察对 G1 控制 STW 的三大机制(Region 选择、参数控制、Mixed GC)和并发标记流程的理解。
**知识点拆解**:
1. **Region 化设计**:堆分成多个 Region(默认 8MB,2MB-32MB),每个 Region 可标记为 Young/Old/Humongous,每次回收部分 Region
2. **Region 选择机制**:按"脏度"排序(垃圾比例高、回收收益大),优先回收垃圾最多的 Region,-XX:G1MixedGCCountTarget 控制 Mixed GC 轮数(默认 8 轮)
3. **参数控制**:-XX:MaxGCPauseMillis=200(目标停顿,G1 动态调整每次回收的 Region 数量)、-XX:InitiatingHeapOccupancyPercent=45(触发并发标记的堆占用率)、-XX:G1HeapRegionSize=8m(Region 大小)
4. **Mixed GC 机制**:Young GC + 部分老年代 Region,避免退化到 FullGC;Mixed GC 跟不上回收速度(老年代增长快于回收速度)才退化到 FullGC
5. **G1 并发标记流程**:Initial Mark(STW)→ Concurrent Mark(并发遍历对象图)→ Final Remark(STW,处理引用变动)→ Cleanup(STW,清理和移动对象)
6. **Remembered Set**:每个 Region 维护跨 Region 引用集合,避免全堆扫描
**考察方向**:
- **深度**:Region 选择机制、参数控制、Mixed GC 触发条件
- **广度**:与 CMS、ZGC 的对比
- **应用**:G1 参数调优(-XX:MaxGCPauseMillis、-XX:G1HeapRegionSize)
**延伸递进面试题**:
1. **G1 和 CMS 有什么区别?**
- 解答要点:G1 是 Region 化 + Mixed GC(可预测停顿),CMS 是并发标记-清除(有碎片);G1 适合 8-32G,CMS 适合 ≤ 8G
2. **G1 的 Mixed GC 和 Full GC 有什么区别?**
- 解答要点:Mixed GC 是 Young GC + 部分老年代 Region,Full GC 是整代回收;Mixed GC 可预测停顿,Full GC 是最后手段
3. **G1 的并发标记流程是怎样的?**
- 解答要点:Initial Mark(STW,标记 GC Roots)→ Concurrent Mark(并发遍历对象图)→ Final Remark(STW,处理引用变动)→ Cleanup(STW,清理和移动对象)
**【一句话结论】**
面试官您好,G1 控制 STW 的三大机制——Region 选择(按脏度排序)、参数控制(-XX:MaxGCPauseMillis=200)、Mixed GC 机制(Young GC + 部分老年代 Region)。
**【三点展开】**
第一,Region 化设计:堆分成多个 Region(默认 8MB),每次回收部分 Region。Region 选择按"脏度"排序(垃圾比例高、回收收益大),优先回收垃圾最多的 Region。-XX:G1MixedGCCountTarget 控制 Mixed GC 轮数(默认 8 轮)。
第二,参数控制:-XX:MaxGCPauseMillis=200 设置目标停顿,G1 动态调整每次回收的 Region 数量(停顿时间短就多回收一些,停顿时间长就少回收一些)。-XX:InitiatingHeapOccupancyPercent=45 触发并发标记(堆占用率超过 45% 启动并发标记)。
第三,Mixed GC 机制:Young GC + 部分老年代 Region,避免退化到 FullGC。Mixed GC 跟不上回收速度(老年代增长快于回收速度)才退化到 FullGC。G1 并发标记流程:Initial Mark(STW)→ Concurrent Mark(并发)→ Final Remark(STW)→ Cleanup(STW)。
**【一句边界】**
当然,G1 适合 8-32G 堆。堆 > 32G 推荐 ZGC(STW < 1ms)。G1 的 Region 内部用 Remembered Set 维护跨 Region 引用集合,避免全堆扫描。-XX:MaxGCPauseMillis 是目标值不是精确值,G1 尽力达到但可能超支。
**核心考点定位**:考察 GC 排查的完整工具链(GC 日志 + heap dump + profiler)和标准排查路径。
**知识点拆解**:
1. **GC 排查五步标准路径**:
- **Step1 监控告警**:CPU > 60% 预警、FullGC > 1 次/小时告警、STW > 500ms 严重、BLOCKED > 50 线程、堆使用率 > 80%
- **Step2 GC 日志分析**:-Xlog:gc*:file=gc.log 看 pause time/heap usage 趋势、jstat -gc <pid> 1000 看 GC 频率、jstat -gccause <pid> 看 GC 原因
- **Step3 线程 dump**:jstack <pid> 看线程状态、BLOCKED 线程卡在哪个 synchronized、RUNNABLE 线程在做什么是 CPU 飙高主因
- **Step4 profiler 热点**:async-profiler/arthas profiler start --event cpu 看热点方法火焰图、定位循环内频繁创建大对象
- **Step5 heap dump 分析**:jmap -dump:live,format=b,file=heap.hprof <pid> 导出堆快照、MAT Dominator Tree 看大对象、泄漏报告定位内存泄漏
2. **数据倾斜导致 FullGC 的根因**:
- **流量倾斜**:热点内容导致单台机器压力,对象创建速度快于回收速度
- **内存泄漏**:某个方法或类不断创建对象但不释放,老年代持续增长
- **大对象频繁创建**:循环内频繁创建大对象(如大数组、大 Map),导致老年代快速增长
3. **修复方案**:
- 流量倾斜:调整负载均衡算法(一致性哈希、最少连接数)、热点数据缓存、读扩散
- 内存泄漏:修复代码(清理缓存、关闭资源)、增大堆内存(治标不治本)、调整 GC 参数
- 大对象频繁创建:对象池化、减少循环内对象创建、使用栈对象(不逃逸分析)
**考察方向**:
- **深度**:五步标准路径、GC 日志分析、heap dump + MAT 分析
- **广度**:与 Q9(P99 飙高)、Q10(CPU95 线程 800)的排查方法对比
- **应用**:线上 GC 排查实战、预警机制设计
**延伸递进面试题**:
1. **如何分析 GC 日志?**
- 解答要点:-Xlog:gc*:file=gc.log 开启 GC 日志,看 pause time(STW 耗时)、heap usage(堆使用趋势)、GC 原因(allocation failure/ metaspace 等)
2. **MAT 如何分析 heap dump?**
- 解答要点:Dominator Tree 看大对象(占据最多内存的对象)、泄漏报告(Leak Suspects)定位内存泄漏、Histogram 看对象数量直方图
3. **数据倾斜如何导致 FullGC?**
- 解答要点:热点内容导致单台机器压力,对象创建速度快于回收速度,老年代快速增长,触发 FullGC
**【一句话结论】**
面试官您好,GC 排查五步标准路径——监控告警 → GC 日志分析 → 线程 dump → profiler 热点 → heap dump 分析。
**【三点展开】**
第一,Step1 监控告警:CPU > 60% 预警、FullGC > 1 次/小时告警、STW > 500ms 严重、BLOCKED > 50 线程、堆使用率 > 80%。先确认是不是流量倾斜(热点内容导致单台机器压力),如果是调整负载均衡算法。
第二,Step2-3 GC 日志 + 线程 dump:-Xlog:gc*:file=gc.log 看 pause time/heap usage 趋势,jstat -gc <pid> 1000 看 GC 频率,jstat -gccause <pid> 看 GC 原因。jstack <pid> 看线程状态,BLOCKED 线程卡在哪个 synchronized,RUNNABLE 线程在做什么是 CPU 飙高主因。
第三,Step4-5 profiler + heap dump:async-profiler/arthas profiler start --event cpu 看热点方法火焰图,定位循环内频繁创建大对象。jmap -dump:live,format=b,file=heap.hprof <pid> 导出堆快照,MAT Dominator Tree 看大对象,泄漏报告定位内存泄漏。
**【一句边界】**
当然,排查要形成标准路径,不要凭直觉。流量倾斜先调整负载均衡,内存泄漏要修复代码(不是增大堆内存),大对象频繁创建要对象池化。预警机制要提前设置(CPU > 60% 预警、FullGC > 1 次/小时告警),避免线上事故。