← 学习门户 每日面试题 学习看板

2026-10-16

JVM / GC 深入 —— 从三色标记到 GC 排查实战

10 题 · 平均 3/5 · low 2.67 · medium 3.5 · high 2.67 · 第 1 轮

#1 什么是三色标记算法? low java

题目

什么是三色标记算法?

我的回答

这个所谓三色标记算法,其实是在 CMS 的垃圾回收器阶段开发使用的。其核心作用是标记对象的存活方式。如果一个对象初始存活创建,做一个标记。当这个对象它首次被访问的时候,但是没有确定它最终的结果是存活还是不存活的时候,做一个标记,应该是灰色吧。最开始是黑色,然后是灰色,然后是白色。如果这个对象标记完确认不能存活,就标记为黑色。应该是白色,初始默认是黑色,标记为白色。这样就可以判定这个对象是否还存活,是否有价值。如果一个对象发生了引用的变化,它会重新重置。整个链路过程就是初始为黑,然后扫描,扫描到它,它如果是没有引用,跟直达的引用,就把它标记为黑,标标记为白色。如果它还有引用,就标记为灰色。最终确认这些对象所有对象的状态,然后对统一的标记完没有引用的颜色的,没有引用的对象进行统一的回收,就是三色标记算法。

点评

优点:- 提到了三色标记**在 CMS 等并发 GC 中使用**(正确的定位) - 提到了**颜色变化与引用扫描相关**的直觉 - 直觉意识到"引用变化要重新标记"(这实际对应写屏障机制)
不足:- **三色定义完全错误**(这是致命问题): - 正确:**白色 = 未被访问/初始状态**、**灰色 = 已访问但子节点未全扫描**、**黑色 = 已访问且子节点扫描完毕** - 用户说反了:说"初始是黑色"、"扫描后是白色"、"没引用标白色" - **回收对象搞反**:三色标记的**白色对象 = 垃圾**(回收对象),不是"没有引用的对象标白然后回收"——白色是**初始状态**,如果没被扫描到,就是垃圾;用户理解为"标白后再回收",逻辑反了 - **灰色含义不清**:灰色是"待扫描子节点"的中间状态,用户理解为"没有确定存活与否"——方向对但术语不准 - **完全没有提并发场景的内存安全**——这才是三色标记存在的意义!**写屏障**修复"黑指白"漏回收问题、**读屏障**修复"白指黑"误回收问题,全部遗漏 - **与 ZGC/Shenandoah 的关联缺失**:ZGC 用读屏障+多映射、Shenandoah 用写屏障+Brooks 转发,都是三色标记的应用 - **没有讲最终状态**:三色标记结束 → 灰色必为 0 → 白色 = 待回收
补充:- **三色严格定义**: - **白色**:从未被 GC 访问过,可能是垃圾也可能是存活的(初始状态) - **灰色**:已被访问,但引用的对象还有未访问的(工作队列) - **黑色**:已访问,且所有引用都已扫描完(确定存活) - **算法流程**:GC Roots 全染色黑 → 遍历黑色节点的引用 → 引用对象由白变灰、加入队列 → 灰节点扫描子节点 → 子节点变灰、灰节点变黑 → 灰队列为空时结束 - **并发问题(重点)**: - **对象消失**(漏回收):黑→白删除 + 灰→黑断链 → 白节点无人引用但活着(**黑指白**) - **对象复活**(误回收):黑←白添加 + 白→灰断链 → 黑节点仍存活但被错认为垃圾 - **两种修复方案**: - **写屏障**(CMS、G1、ZGC 传统模式):修复黑指白,规则"**对象引用变白→父对象重染灰**" - **读屏障**(Shenandoah、ZGC 现代模式):修复白指黑,规则"**读对象指针时检查有效性,无效则重新标记为黑**" - **ZGC 的启示**:STW 只有 Initial Mark 和 Final Relocate 两个瞬间,靠读屏障+多映射让对象移动不改引用

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察三色标记的**严格定义**+**并发场景的内存安全问题**+**修复机制(写屏障/读屏障)**。

**知识点拆解**:

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 遍历;区别在是否并发、如何处理引用变动

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,**三色标记算法**是并发 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 遍历,但通过三色状态管理并发下的引用变动,靠写屏障或读屏障保证内存安全。

---

#2 YoungGC和FullGC的触发条件是什么? low java

题目

YoungGC和FullGC的触发条件是什么?

我的回答

ygc的触发条件是年轻代空间达到满溢比例,然后开始进行年轻代中eden区和s1区的回收; fgc触发条件就相对较多了,老年代空间使用到一定比例应该是80%会触发fgc进行老年代回收,这里不同的垃圾回收器有不同的处理比如G1就引入了mgc,提前会做一次老年代部分空间的回收,而不是直接触发fgc; 另外方法区出现空间不足也会引起fgc进行垃圾回收;

点评

优点:- YoungGC 触发条件正确(Eden 满) - 提到 G1 的 Mixed GC 作为 FGC 替代方案,认知到位 - 提到 Metaspace 不足触发 FGC
不足:- FGC 触发条件不全:缺晋升失败(Survivor 装不下)、System.gc() 显式调用、G1 Mixed GC 跟不上退化 - 80% 阈值是经验值不是固定值,实际是老年代空间不足时触发 - 未提 FGC 的避免策略(增大老年代、调整晋升阈值、G1 Mixed GC 替代)
补充:完整 FGC 触发条件有 6 种,避免策略有 3 种。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 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),生产环境应禁用

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,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 是最后手段,频繁触发说明有内存泄漏或参数配置不当。

#3 什么是跨代引用,有什么问题? low java

题目

什么是跨代引用,有什么问题?

我的回答

跨代引用指的是年轻代的对象引用了老年代的对象,或者老年代的对象引用了年轻代的对象,这时候年轻的对象进行ygc的时候无法正常回收,没法回收老年代的空间,导致空间无法正常释放,对象可能会直接从年轻代进入老年代

点评

优点:- 提到了跨代引用的概念(年轻代和老年代之间的引用)
不足:- **概念混淆**:跨代引用特指 Old→Young 引用(不是双向),不是"年轻代引用老年代" - **问题理解错误**:跨代引用的问题不是"YGC 回收不了老年代",而是 FullGC 时若从老年代 GC Roots 遍历新生代会 O(n) 爆炸 - **解决方案缺失**:未提 Remembered Set(G1)和 Card Table(CMS/Parallel)+ 写屏障 - 把"对象直接从年轻代进入老年代"(晋升)和跨代引用混淆
补充:跨代引用是单向的(Old→Young),通过 Remembered Set/Card Table + 写屏障解决。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对跨代引用的严格定义、问题本质和解决机制的理解。

**知识点拆解**:
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) 爆炸)

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,跨代引用特指老年代对象引用新生代对象,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 维护)。

#4 Java的堆是如何分代的?为什么分代? medium java

题目

Java的堆是如何分代的?为什么分代?

我的回答

Java 堆的分代,其核心问题是要解决对象存活时间不同的问题。所以它主要划分的方式是年轻代跟老年代。年轻代就是刚建立的对象存储的空间,老年代就是经历了一段时间还存活下来或者比较大的对象存储的空间。这些比较大的对象存储空间存储到老年代的原因是因为年轻代的空间有限。然后为了减少反复的它的反复存活,多次服用复制,可以直接升到老年代。为什么要分代?核心原因其实是对不同类型的对象管理方式的不同。年轻代的对象是朝生夕死,快速生快速死。快速的回收就释放了空间,这样可以更好的管理,更好的释放空间。而老年代存储的是一些,这个对象存活时间比较长了,然后把它在老年代中去存放,让它更稳定一些。只有当 Full GC 的时候才会去动这些老年代的对象,而不是经常性的通过 Young GC 反复的去对它进行操作,降低操作的时间,率。

点评

优点:- 核心逻辑清晰:**分代是为了不同对象用不同算法**(存活时间差异 → 不同管理方式),抓住了 GC 设计的核心权衡 - 提到大对象直接进老年代(避免复制开销),触及到 PretenureSizeThreshold 的动机 - "朝生夕死"术语准确,弱分代假说的关键词命中
不足:- **新生代内部结构缺失**:没提到 Eden + From Survivor + To Survivor(8:1:1)的具体结构,这是"如何分代"的必答部分 - **晋升机制缺失**:没说清对象怎么从 Eden → Survivor → Old 迁移,**年龄计数器**(每次 GC +1,超过 MaxTenuringThreshold=15 晋升)是关键 - **Full GC 描述不准**:说"只有 Full GC 才动老年代"是错的,**Mixed GC**(G1 特有)也会回收部分老年代 Region;还有**晋升触发**(To Survivor 装不下就晋升) - **缺少术语**:弱分代假说、强分代假说、Eden/Survivor/MaxTenuringThreshold/PretenureSizeThreshold 这些关键词面试官想听 - **未提跨代引用问题**:老年代→新生代的引用(Remembered Set / Card Table)是分代设计的复杂性来源,属于延伸加分点
补充:- 完整分代结构: - 新生代:Eden (80%) + From Survivor (10%) + To Survivor (10%) - 老年代:Old Gen / Tenured Gen - Metaspace(永久代之后的新设计) - 晋升条件(4 种): 1. **年龄晋升**:GC 次数达 MaxTenuringThreshold(默认 15) 2. **动态年龄判定**:Survivor 中同龄对象总和 > Survivor 空间 50%,该年龄及更大直接晋升 3. **大对象晋升**:超过 PretenureSizeThreshold 直接在老年代分配 4. **空间分配担保**:Survivor 装不下就把对象搬到老年代 - Full GC vs Mixed GC:Full GC 整代回收(STW 长),Mixed GC 是 G1 在 Young GC 基础上选几个老年代 Region 一起回收(可预测停顿) - 跨代引用:老年代对象引用新生代对象时,通过 Remembered Set 或 Card Table 记录,避免 Full GC 时全堆扫描

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察分代机制的结构性理解(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 分区),进一步降低停顿、提高吞吐

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,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**,进一步降低停顿、提高吞吐。

---

#5 介绍下CMS的垃圾回收过程 medium java

题目

介绍下CMS的垃圾回收过程

我的回答

CMS垃圾回收器已经很少使用了,基本也不推荐使用,现在堆分配内存空间在8到32G的基本就使用G1,在向上的就用ZGC; CMS更接近一个中间产物,在9的时候已经不用了,jdk14就已经移除了; CMS期望的是垃圾回收和系统使用一起进行,不发生STW,但目标是好的实现起来就没那么容易了; CMS是老年代的回收算法,整个回收过程有4个阶段初始标记,会短暂的进行STW,标记一批次重要的对象,然后并行标记,这个阶段不做stw,然后是再次标记,又一次进行stw,最后是垃圾回收,期望是通过分批次的标记,降低gc的stw,但是因为并行执行,会引入很多二次变动的问题,而且CMS使用的是标记清理算法,会产生很多内存碎片,当无法正常通过CMS算法回收后还会退回到单线程的垃圾回收

点评

优点:- **四阶段讲清楚**:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 清理,STW 分布准确 - **局限性认知到位**:并发导致引用变动、标记-清除产生碎片、退化到 Serial Old - **GC 器选型认知好**:8-32G 用 G1,更大用 ZGC,CMS 已移除(JDK 14) - **期望 vs 现实**:CMS 期望无 STW,但实现有两次 STW
不足:- 未提 CMS 的触发条件(老年代使用率达阈值,默认 70%) - 未提 CMS 的写屏障机制(处理并发引用变动) - 未提 CMS 的碎片化问题会导致提前 FullGC(晋升失败) - 未提 CMS 的 CPU 占用高(并发标记需要多个线程)
补充:CMS 是并发标记-清除,适合堆 ≤ 8G、停顿敏感的场景。JDK 9+ 废弃,JDK 14 移除。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 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 更优

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,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 已移除,新项目不应使用。

#6 G1如何精确控制STW的时间的? medium java

题目

G1如何精确控制STW的时间的?

我的回答

G1可以通过参数指定具体的预期垃圾回收时间,由于G1在设计的时候选择使用的方式是把内存空间分开分成大量的独立小空间进行分别管理; 在GC回收时每个小空间都进行相应的空间整理,G1通过控制小空间的回收比例和次数来实现一定范围内的控制STW时间,另外G1引入了MGC,来实现对部分老年代空间的回收,而不是直接进行全量的FGC,也使得其STW时间更可控; 控制STW时间的核心就是控制一次GC的范围,通过控制回收范围来实现控制STW时间

点评

优点:- **Region 化设计理解到位**:堆分成多个 Region,每次回收部分 Region - **Mixed GC 认知好**:Young GC + 部分老年代 Region,避免退化到 FullGC - **核心思路正确**:控制回收范围 = 控制 STW 时间 - **参数控制认知**:-XX:MaxGCPauseMillis 设置目标停顿
不足:- **缺具体机制**:Region 选择(按"脏度"排序)、-XX:MaxGCPauseMillis 参数、并发标记流程 - **缺 Mixed GC 触发条件**:Mixed GC 跟不上回收速度才退化到 FullGC - **缺 Region 内部机制**:Remembered Set 跨 Region 引用集合 - **缺 G1 并发标记流程**:Initial Mark STW → Concurrent Mark 并发 → Final Remark STW → Cleanup STW
补充:G1 控制 STW 的三大机制——Region 选择(按脏度排序)、参数控制(-XX:MaxGCPauseMillis=200)、Mixed GC 机制(Young GC + 部分老年代 Region)。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 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,清理和移动对象)

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,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 尽力达到但可能超支。

#7 短链5亿次生成20亿跳转P99-50ms medium practice

题目

短链5亿次生成20亿跳转P99-50ms

我的回答

暂无回答记录

点评

暂无点评

完整讲解

暂无完整讲解
#8 数据倾斜导致的频繁FullGC问题排查 high practice

题目

数据倾斜导致的频繁FullGC问题排查

我的回答

数据倾斜下导致频繁fgc,需要首先查看下是否是流量倾斜,是否是有热点内容访问到某一台机器,导致请求都倾斜到一台服务上; 前置应该是发现了一组集群中某一台机器出现频繁fgc,然后可以先查看服务器负载的看板,看是否是请求流量发生倾斜,如果是请求流量出现的倾斜,那么需要调整前置的负载均衡算法,将流量平均下; 如果流量层没有问题,但是某台机器出现频繁fgc,那么使用jmap或者arthas导出服务的内存快照,使用jstack导出线程快照,检查下内存对象分配的情况; 确认具体是什么样的原因导致的频繁fgc;定位到具体的方法,和内容;然后对应进行分析处理

点评

优点:- 先排查流量倾斜的意识好(热点内容导致单台机器压力) - 提到 jmap/jstack 导出内存快照和线程快照 - 排查顺序基本正确(流量层 → JVM 层)
不足:- **缺 GC 日志分析**:未提 jstat -gcutil、GC 日志(pause time、heap usage 趋势) - **缺 heap dump + MAT 分析**:未提 MAT Dominator Tree 分析大对象、泄漏报告定位内存泄漏 - **缺 profiler 热点方法定位**:未提 async-profiler/arthas profiler 看热点方法火焰图 - **缺具体指标**:未提 FullGC 频率、STW 耗时、BLOCKED 线程数等关键指标 - **排查步骤过于笼统**:未形成"监控 → GC 日志 → 线程 dump → profiler → heap dump"标准路径
补充:GC 排查五步标准路径——监控告警 → GC 日志分析 → 线程 dump → profiler 热点 → heap dump 分析。

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察 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

第三部分:面试者参考回答(三步法口语化,300-400 字)

**【一句话结论】**
面试官您好,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 次/小时告警),避免线上事故。

#9 P99飙到2s与FullGC频繁CPU排查 high practice

题目

P99飙到2s与FullGC频繁CPU排查

我的回答

暂无回答记录

点评

暂无点评

完整讲解

暂无完整讲解
#10 CPU95线程800的GC连锁反应排查 high practice

题目

CPU95线程800的GC连锁反应排查

我的回答

暂无回答记录

点评

暂无点评

完整讲解

暂无完整讲解

📚 查看全部面试记录