面试必备:JVM 垃圾回收算法详解
如何判断对象已死?
GC 之前要先确定"哪些对象是垃圾"。
1. 引用计数法
// 每个对象有个引用计数器
// 对象被引用 +1,引用失效 -1
// 计数器为 0 → 垃圾
Object a = new Object(); // count = 1
Object b = a; // count = 2
a = null; // count = 1
b = null; // count = 0 → 可以回收
问题:无法解决循环引用
Object A = new Object();
Object B = new Object();
A.ref = B; // A 引用 B
B.ref = A; // B 引用 A
A = null;
B = null;
// 此时 A 和 B 都不用了,但相互引用,计数器都不是 0
// 引用计数法无法回收 → 被淘汰
2. 可达性分析(根搜索算法)
从 GC Roots 开始,向下搜索,形成"引用链"。
不在引用链上的对象 → 垃圾
GC Roots
│
┌─────┴─────┐
│ │
[A] [B]
│ │
▼ ▼
[C] [D] ←─ 图中 C、D 形成引用链
│ │
▼ ▼
[E] [F]
│
▼
[G] ←─ 这个对象没有被 GC Roots 引用
→ 可以回收
GC Roots 包括:
- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈 JNI 引用的对象
- JVM 内部引用(ClassLoader、异常对象)
- 同步锁持有的对象
3. 四种引用类型
| 类型 | 说明 | 回收时机 |
|---|---|---|
| 强引用 | Object o = new Object() | 永远不会回收 |
| 软引用 | SoftReference | 内存不足时回收 |
| 弱引用 | WeakReference | 下次 GC 时回收 |
| 虚引用 | PhantomReference | 随时可能被回收 |
// 软引用:缓存场景
SoftReference<byte[]> cache = new SoftReference<>(new byte[1024*1024]);
// 弱引用:ThreadLocal
WeakReference<Object> ref = new WeakReference<>(new Object());
// 下次 GC 必定回收
垃圾回收算法
1. 标记-清除算法(Mark-Sweep)
标记阶段:标记所有存活对象
清除阶段:回收所有未标记对象
before:
┌──────────────────────────────┐
│ ○ ● ○ ● ● ○ ● ○ │
│ ↑ ↑ ↑ │
│ 存活 存活 存活 │
└──────────────────────────────┘
↓
after:
┌──────────────────────────────┐
│ ○ ○ ○ ● ● ● ○ ○ │
│ 空闲 空闲 使用 空闲 │
└──────────────────────────────┘
缺点:
- 效率不稳定
- 产生内存碎片
2. 复制算法(Copying)
把内存分成两块,每次只用一块
GC 时,把存活对象复制到另一块,然后整块清除
内存 A (使用中) 内存 B (空闲)
┌────────────────┐ ┌────────────────┐
│ ● ○ ● ○ │ → │ ● ● ● ● │
│ 存活复制过来 │ │ 原样复制 │
└────────────────┘ └────────────────┘
然后把内存 A 全部清空
优点:没有碎片,效率高
缺点:可用内存减半(实际用 8:1:1 优化)
3. 标记-整理算法(Mark-Compact)
标记阶段:和标记-清除一样
整理阶段:让存活对象向一端移动
before: 整理中: after:
┌──────────┐ ┌──────────┐ ┌──────────┐
│●●○●●○○●●│ │●●●●●●○○○│ │●●●●●●○○○│
└──────────┘ └──────────┘ └──────────┘
→→→→→ 存活对象密集
优点:没有碎片,不用浪费一半内存
缺点:整理需要时间,效率略低
4. 分代收集算法
根据对象存活周期分代处理:
┌──────────────────────────────────────────────┐
│ 堆内存 │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ Young Gen │ │ Old Gen │ │
│ │ (新生代) │ │ (老年代) │ │
│ │ │ │ │ │
│ │ ┌──────────┐ │ │ │ │
│ │ │ Eden │ │ │ │ │
│ │ │ 80% │ │ │ │ │
│ │ ├──────────┤ │ │ │ │
│ │ │ S0 │ S1 │ │ │ │ │
│ │ │10% │10% │ │ │ │ │
│ │ └──────────┘ │ │ │ │
│ │ │ │ │ │
│ │ Minor GC │ │ Major GC │ │
│ │ 频繁、较快 │ │ /Full GC │ │
│ │ 复制算法 │ │ 较慢、标记整理 │ │
│ └────────────────┘ └──────────────────┘ │
│ │
└──────────────────────────────────────────────┘
年轻代:对象朝生夕死,用复制算法
老年代:对象存活时间长,用标记-清除/整理算法
常见垃圾收集器
年轻代收集器:
├── Serial(单线程)
├── ParNew(多线程)
└── Parallel Scavenge(吞吐量优先)
老年代收集器:
├── Serial Old
├── Parallel Old
├── CMS(并发标记清除)
└── G1(Garbage First)
组合关系:
┌────────────────────────────────────────────────────────┐
│ Serial ──── Serial Old │
│ Serial ──── CMS │
│ ParNew ──── CMS │
│ ParNew ──── Serial Old │
│ Parallel Scavenge ──── Parallel Old │
│ G1(统一) │
└────────────────────────────────────────────────────────┘
1. Serial / Serial Old
- 单线程,最简单
- Stop The World(STW):GC 时暂停所有用户线程
- 桌面应用,单核服务器
2. ParNew(年轻代)
- Serial 的多线程版
- 默认收集线程 = CPU 核数
- JDK 7/8 推荐与 CMS 搭配
3. Parallel Scavenge(年轻代)
- 关注吞吐量(吞吐量 = 运行代码时间 / (运行代码时间 + GC 时间))
- 支持自适应调节(-XX:+UseAdaptiveSizePolicy)
4. CMS(老年代)
四步走:
① 初始标记(STW)→ 标记 GC Roots 直接引用的对象
② 并发标记 → 从 GC Roots 向下追踪(用户线程运行)
③ 重新标记(STW)→ 修正并发标记期间变动的对象
④ 并发清除 → 清除垃圾(用户线程运行)
优点:并发收集,低停顿
缺点:
- CPU 敏感
- 无法处理浮动垃圾(并发清理阶段产生的垃圾)
- 产生内存碎片
5. G1(Garbage First)
JDK 9+ 默认收集器
特点:
- 把堆分成多个大小相等的 Region
- 跟踪各 Region 垃圾堆积价值(回收能释放多少空间)
- 优先回收价值最高的 Region
四步:
① 初始标记(STW)→ 标记 GC Roots
② 并发标记 → 追踪存活对象
③ 最终标记(STW)→ 修正标记
④ 筛选回收 → 根据价值排序,回收部分 Region
面试高频问题
Q1:Minor GC 和 Full GC 的区别?
| 类型 | 触发条件 | 频率 | 停顿时间 |
|---|---|---|---|
| Minor GC | Eden 满 | 高 | 短 |
| Full GC | 老年代满 / Metaspace 满 / System.gc() | 低 | 长 |
Q2:对象进入老年代的时机?
- 年龄达到阈值(-XX:MaxTenuringThreshold,默认 15)
- 大对象直接分配
- Survivor 中相同年龄对象 > Survivor 的 50%
Q3:CMS 和 G1 的区别?
| 维度 | CMS | G1 |
|---|---|---|
| 目标 | 老年代 | 全堆 |
| 算法 | 标记-清除 | 标记-整理 |
| 碎片 | 有碎片 | 无碎片 |
| 停顿 | 可预测 | 可预测 |
| JDK 版本 | 1.5+ | 9+默认 |
总结
判断对象死亡:
- 引用计数(无法处理循环引用)
- 可达性分析(从 GC Roots 追踪)
垃圾回收算法:
- 标记-清除(碎片)
- 复制算法(年轻代,无碎片,需浪费一半空间)
- 标记-整理(老年代,无碎片)
分代收集:年轻代复制算法,老年代标记-整理
收集器:Serial → ParNew/Parallel Scavenge → CMS → G1