JVM知识梳理(2)
作者:没有四次元口袋的蓝胖
日期:2026-07-31
标签:Java, JVM, GC算法, CMS, G1
JVM 垃圾回收是面试重点。核心掌握:如何判断对象可回收、四种 GC 算法、CMS 和 G1 回收器的区别。
一、如何判断对象可以回收?
1.1 引用计数法
每个对象维护一个引用计数器,被引用时 +1,引用失效时 -1,为 0 时可回收。
java
Object obj = new Object(); // 引用计数 = 1
Object ref = obj; // 引用计数 = 2
ref = null; // 引用计数 = 1
obj = null; // 引用计数 = 0 → 可回收
问题:循环引用导致计数永远不为 0。
java
Object a = new Object();
Object b = new Object();
a.ref = b;
b.ref = a;
a = null;
b = null;
// 引用计数都是 1,但实际已无法访问 → 内存泄漏
1.2 可达性分析(Java 采用)
从 GC Roots 开始向下搜索,搜索过的路径称为引用链。如果一个对象到 GC Roots 没有任何引用链,说明不可达,可回收。
GC Roots(根节点集合)
├── 虚拟机栈中的引用(局部变量)
├── 方法区中静态属性引用
├── 方法区中常量引用
├── 本地方法栈中 JNI 引用
└── 被同步锁持有的对象
二、四种垃圾回收算法
2.1 标记-清除(Mark-Sweep)
过程:
-
标记所有需要回收的对象
-
统一清除被标记的对象
回收前:[A][B][C][D][E] (B、D 是垃圾)
标记后:[A][✗][C][✗][E]
清除后:[A][ ][C][ ][E] ← 内存不连续
优点 :简单
缺点 :产生大量不连续的内存碎片,后续分配大对象时可能找不到连续空间
适用:老年代(对象存活率高)
2.2 复制(Copying)
过程:
-
将内存分为大小相等的两块
-
每次只使用一块
-
回收时,将存活对象复制到另一块
-
清空当前使用的块
回收前:
From 区:[A][B][C][D] (B、D 是垃圾)
To 区:[ ][ ][ ][ ]复制后:
From 区:[ ][ ][ ][ ] ← 清空
To 区:[A][C][ ][ ] ← 存活对象
优点 :无内存碎片,分配效率高(指针碰撞)
缺点:浪费一半内存
改进(实际采用):Eden + 两个 Survivor(8:1:1)
- Eden 占 80%,Survivor 各占 10%
- 每次使用 Eden + 一个 Survivor
- 回收时,将存活对象复制到另一个 Survivor
- 只浪费 10% 内存
适用:新生代(对象朝生夕死,存活率低)
2.3 标记-整理(Mark-Compact)
过程:
-
标记所有需要回收的对象
-
将存活对象向一端移动
-
清理边界外的内存
回收前:[A][B][C][D][E] (B、D 是垃圾)
标记后:[A][✗][C][✗][E]
整理后:[A][C][E][ ][ ] ← 内存连续
优点 :无内存碎片
缺点 :移动对象开销大(需要更新引用)
适用:老年代(对象存活率高,移动成本低)
2.4 分代回收(Generational Collection)
核心思想:根据对象存活年代选择不同的算法。
| 区域 | 对象特点 | 采用算法 |
|---|---|---|
| 新生代 | 朝生夕死,存活率低 | 复制算法(效率高) |
| 老年代 | 存活率高,移动成本大 | 标记-清除或标记-整理 |
面试回答模板:分代回收是为了提高效率。新生代对象大部分都死,用复制算法每次只复制少量存活对象;老年代对象存活率高,用标记-整理避免碎片。
三、CMS 回收器(Concurrent Mark Sweep)
3.1 特点
- 目标:最短回收停顿时间
- 算法:标记-清除
- 适用:老年代
- 并发回收:回收过程中用户线程继续运行
3.2 四个阶段
1. 初始标记(Initial Mark)
└─ 标记 GC Roots 直接关联的对象
└─ STW(Stop The World,暂停用户线程)
└─ 速度快
2. 并发标记(Concurrent Mark)
└─ 从 GC Roots 开始遍历整个对象图
└─ 用户线程并发执行
└─ 耗时最长
3. 重新标记(Remark)
└─ 修正并发标记期间变动的对象
└─ STW
└─ 时间比初始标记长
4. 并发清除(Concurrent Sweep)
└─ 清除被标记的垃圾对象
└─ 用户线程并发执行
3.3 优缺点
优点:
- 并发收集、低停顿
- 适合互联网应用(对响应时间敏感)
缺点:
| 问题 | 原因 | 解决 |
|---|---|---|
| CPU 资源敏感 | 并发阶段占用 CPU | -XX:ParallelGCThreads 控制线程数 |
| 浮动垃圾 | 并发清除阶段新产生的垃圾 | 预留内存空间 |
| 内存碎片 | 标记-清除算法 | Full GC 时整理 |
| 并发失败 | 内存不足 → Full GC → STW | 调大堆或换 G1 |
四、G1 回收器(Garbage First)
4.1 特点
- 目标:可预测的停顿时间模型
- 算法:标记-整理(整体)+ 复制算法(局部)
- 适用:全堆(新生代 + 老年代)
- JDK 9 成为默认回收器
4.2 核心设计:Region 划分
G1 把堆划分为多个大小相等的独立区域(Region):
堆(Heap)
┌─────┬─────┬─────┬─────┬─────┐
│ Eden│ Eden│ S0 │ S1 │ Old │
├─────┼─────┼─────┼─────┼─────┤
│ Old │ Old │ Hum │ Hum │ Old │
├─────┼─────┼─────┼─────┼─────┤
│ Old │ Old │ Old │ Old │ Old │
└─────┴─────┴─────┴─────┴─────┘
Region 类型:
- Eden:新生代对象
- Survivor:存活对象
- Old:老年代对象
- Hum:Humongous(大对象,超过 Region 50%)
关键点:每个 Region 既可能是新生代,也可能是老年代,不再物理隔离。
4.3 回收过程
1. 初始标记(STW)
└─ 标记 GC Roots 直接关联的对象
2. 并发标记
└─ 遍历对象图,统计各 Region 存活对象价值
3. 最终标记(STW)
└─ 修正并发标记期间变动
4. 筛选回收
└─ 根据用户设定的停顿时间,选择回收价值最高的 Region
└─ 复制存活对象,清空垃圾 Region
核心思想:优先回收垃圾最多的 Region(Garbage First 名字的由来)。
4.4 CMS vs G1 对比
| 维度 | CMS | G1 |
|---|---|---|
| 适用区域 | 老年代 | 全堆 |
| 算法 | 标记-清除 | 标记-整理 + 复制 |
| 内存碎片 | 有 | 无(整理过) |
| 停顿预测 | 不支持 | 支持(-XX:MaxGCPauseMillis) |
| 适用场景 | 小堆(<8G)、低延迟 | 大堆(≥8G)、可预测停顿 |
🗺️ 思维导图速览
JVM 垃圾回收
├── 判断对象存活
│ ├── 引用计数法(循环引用问题)
│ └── 可达性分析(GC Roots)
├── 四种算法
│ ├── 标记-清除(碎片问题,老年代)
│ ├── 复制(新生代,Eden+S0+S1)
│ ├── 标记-整理(老年代,无碎片)
│ └── 分代回收(新生代复制+老年代标记-整理)
└── 回收器
├── CMS(并发标记清除,低延迟,有碎片,适合小堆)
└── G1(Region 划分,可预测停顿,JDK 9+ 默认,适合大堆)
📝 写在最后
学习建议
- GC 算法要理解区别:标记-清除有碎片、复制浪费内存、标记-整理移动开销大,面试会问"为什么新生代用复制、老年代用标记-整理"
- CMS 和 G1 要对比:CMS 适合小堆低延迟但有碎片,G1 Region 划分可预测停顿无碎片
- JDK 9+ 默认 G1:这个结论要记住,面试经常问
面试回答模板
Q:说说 JVM 的垃圾回收算法?
JVM 有四种垃圾回收算法。标记-清除算法简单但会产生内存碎片;复制算法把内存分成两块,每次只使用一块,回收时把存活对象复制到另一块,适合新生代;标记-整理算法先标记再移动存活对象,无碎片但移动开销大,适合老年代;分代回收结合两者优点,新生代用复制算法,老年代用标记-清除或标记-整理。
Q:CMS 和 G1 的区别?
CMS 是并发标记清除回收器,适合老年代,追求低延迟但会产生内存碎片;G1 是 Garbage First 回收器,把堆划分为多个 Region,整体看是标记-整理,局部是复制算法,支持可预测的停顿时间模型,JDK 9 成为默认回收器。CMS 适合小堆(<8G),G1 适合大堆(≥8G)。