JVM 垃圾回收算法详解
垃圾回收是 JVM 最重要的机制之一,也是面试高频考点
🎯 面试重点
- 各垃圾回收算法的原理和优缺点
- 各算法的适用场景
- 为什么需要分代回收
📖 垃圾回收概述
什么是垃圾?
/**
* 垃圾:无用的对象占据着内存空间
* 判断对象是否存活的算法:
* 1. 引用计数算法
* 2. 可达性分析算法
*/
public class GCHellowWorld {
public static void main(String[] args) {
// 循环引用示例
Object a = new Object();
Object b = new Object();
a = null; // a 变成垃圾
b = null; // b 变成垃圾
// 此时两个对象互相引用,但都没有外部引用,仍然是垃圾
}
}
引用计数算法
/**
* 原理:对象每被引用一次,计数器+1;引用失效时-1
* 缺点:无法处理循环引用
*/
public class ReferenceCounting {
public Object instance;
public static void main(String[] args) {
// 循环引用问题
ReferenceCounting objA = new ReferenceCounting();
ReferenceCounting objB = new ReferenceCounting();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 此时两个对象引用计数都不为0,但实际上已经不可达
// 纯引用计数算法无法识别这种垃圾
}
}
可达性分析算法(主流)
/**
* 原理:从 GC Root 出发,可达的对象存活,不可达的对象是垃圾
*
* GC Root 包含:
* - 虚拟机栈中引用的对象
* - 方法区中静态属性引用的对象
* - 方法区中常量引用的对象
* - 本地方法栈中 Native 方法引用的对象
*/
public class GCReachability {
// 静态属性 - GC Root
private static Object staticObject;
// 局部变量 - GC Root
private Object instance;
public static void main(String[] args) {
// 可达
staticObject = new Object();
// 不可达(局部变量随着方法结束出栈)
Object local = new Object();
local = null;
}
}
📖 垃圾回收算法
1. 标记-清除算法(Mark-Sweep)
/**
* 过程:
* 1. 标记阶段:标记所有需要回收的对象
* 2. 清除阶段:统一清除被标记的对象
*
* 缺点:
* - 效率低(标记和清除都慢)
* - 空间碎片化(产生不连续的内存)
*/
public class MarkSweepDemo {
/*
* 标记前:
* [对象A][空闲][对象B][对象C][空闲]
*
* 标记后(假设B是垃圾):
* [对象A][空闲][XXXXX][对象C][空闲]
*
* 清除后:
* [对象A][空闲 ][对象C][空闲]
* ↑ 产生内存碎片
*/
}
图示:
标记阶段 清除阶段
┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐
│ A │ B │ C │ │ → │ A │ │ C │ │
└───┴───┴───┴───┘ └───┴───┴───┴───┘
✓ ✗ ✓ ✓ ✗ ✓
结果:产生内存碎片
2. 复制算法(Copying)
/**
* 原理:将内存分为两半,每次只使用一半
* 存活对象复制到另一半,然后清除原半区
*
* 优点:无碎片、简单高效
* 缺点:可用内存减半
*
* 商用实现:Eden + Survivor(8:1:1)
*/
public class CopyingDemo {
/*
* 初始状态:
* ┌─────────────────────────┬─────────────────────────┐
│ From 区 │ To 区 │
│ [A][B][C][D][存活对象] │ [空] │
│ (需要回收) │ │
* └─────────────────────────┴─────────────────────────┘
*
* 复制后:
* ┌─────────────────────────┬─────────────────────────┐
│ From 区 │ To 区 │
│ [空] │ [A'][B'][存活对象'] │
* └─────────────────────────┴─────────────────────────┘
*/
}
图示:
复制算法工作流程
┌─────────────────┐ ┌─────────────────┐
│ 内存块A │ │ 内存块B │
│ [A][B][C][存活] │ 复制 │ [A'][B'][C'] │
└────────┬────────┘ ──→ └─────────────────┘
│ 清除
↓
┌─────────────────┐
│ 内存块A │
│ [全部清空] │
└─────────────────┘
3. 标记-整理算法(Mark-Compact)
/**
* 原理:标记后移动存活对象到一端,然后清理边界外的内存
*
* 优点:无碎片
* 缺点:移动对象开销大(需要更新引用)
*
* 适用:老年代(对象存活率高)
*/
public class MarkCompactDemo {
/*
* 标记后:
* [A][B][C][空闲][D][E][空闲]
* ↑
* 存活对象
*
* 整理后:
* [A][B][C][D][E][空闲 ]
* ↑
* 整理边界
*/
}
4. 分代收集算法(Generational Collection)
/**
* 核心思想:不同对象生命周期不同,采用不同的收集策略
*
* ┌─────────────────────────────────────────┐
│ 堆内存 │
│ ┌─────────────┬───────────────────────┐│
│ │ 新生代 │ 老年代 ││
│ │ (Minor GC) │ (Major/Full GC) ││
│ │ 复制算法 │ 标记-整理算法 ││
│ └─────────────┴───────────────────────┘│
│ 新生代 90% 对象朝生夕死 │
│ 老年代 多次 GC 后仍存活 │
└─────────────────────────────────────────┘
*/
public class GenerationalDemo {
public static void main(String[] args) {
// 新生代对象:短生命周期
for (int i = 0; i < 100; i++) {
String s = "temp" + i; // 使用完立即成为垃圾
}
// 老年代对象:长生命周期
List<String> cache = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
cache.add("persistent" + i); // 长期存活
}
}
}
📖 垃圾收集器
新生代收集器
/**
* Serial 收集器(单线程)
* - 复制算法
* - Stop The World
* - 简单高效(无线程开销)
* - 客户端模式
*/
public class SerialGC {
// -XX:+UseSerialGC
}
/**
* ParNew 收集器(多线程)
* - 复制算法
* - Stop The World
* - 可与 CMS 配合
* - 服务端模式
*/
public class ParNewGC {
// -XX:+UseParNewGC
}
/**
* Parallel Scavenge 收集器
* - 复制算法
* - 吞吐量优先
* - 自适应调节
*/
public class ParallelScavengeGC {
// -XX:+UseParallelGC
// -XX:MaxGCPauseMillis=100 // 最大停顿时间
// -XX:GCTimeRatio=19 // 吞吐量
}
老年代收集器
/**
* Serial Old 收集器
* - 标记-整理算法
* - 单线程
*/
public class SerialOldGC {
// -XX:+UseSerialOldGC
}
/**
* Parallel Old 收集器
* - 标记-整理算法
* - 多线程
*/
public class ParallelOldGC {
// -XX:+UseParallelOldGC
}
/**
* CMS 收集器(Concurrent Mark Sweep)
* - 标记-清除算法
* - 并发收集、低停顿
* - 浮动垃圾问题
* - 内存碎片问题
*/
public class CMSGC {
// -XX:+UseConcMarkSweepGC
/*
* 收集过程:
* 1. 初始标记(STW) - 标记 GC Root
* 2. 并发标记 - 追踪存活对象
* 3. 重新标记(STW) - 修正变化
* 4. 并发清除 - 清除垃圾
*/
}
/**
* G1 收集器(Garbage First)
* - 复制+标记-整理
* - 区域(Region)概念
* - 可预测停顿
* - JDK 9+ 默认
*/
public class G1GC {
// -XX:+UseG1GC
// -XX:MaxGCPauseMillis=200
}
各收集器对比
| 收集器 | 区域 | 算法 | 特点 | 适用场景 |
|---|---|---|---|---|
| Serial | 新生代 | 复制 | 单线程、Client | 小型应用 |
| ParNew | 新生代 | 复制 | 多线程、Server | 多核 + CMS |
| Parallel | 新生代 | 复制 | 吞吐量优先 | 后台计算 |
| Serial Old | 老年代 | 整理 | 单线程 | 小型应用 |
| Parallel Old | 老年代 | 整理 | 多线程 | 后台计算 |
| CMS | 老年代 | 清除 | 并发、低停顿 | 互联网 |
| G1 | 全代 | 整理+复制 | 可预测停顿 | JDK 9+ |
📖 面试真题
Q1: 为什么分代回收?
答: 根据对象生命周期特点,90% 的对象是短命鬼(朝生夕死),只有少数对象存活时间长。新生代用复制算法(高效处理死亡对象),老年代用标记-整理(适合存活对象多的情况)。
Q2: G1 有什么特点?
答:
- 区域(Region)概念:将堆划分为多个大小相等的区域
- 卡片(Card):每个 Region 进一步划分,用于并发标记
- 可预测停顿:支持设置最大停顿时间
- 软实时:优先回收垃圾最多的区域
- 并发标记:减少 STW 时间
- JDK 9+ 成为默认收集器
Q3: CMS 有什么问题?
答:
- 浮动垃圾:并发清理阶段产生的垃圾无法本次清理
- 内存碎片:标记-清除算法导致碎片,需要 Full GC 整理
- 并发失败:预留内存不够时退化为 Serial Old
Q4: 对象分配过程?
答:
- 优先在 Eden 区分配
- 大对象(超过阈值)直接进入老年代
- 长期存活对象进入 Survivor 区
- 对象 age 超过阈值(默认 15)进入老年代
Q5: 垃圾回收算法有哪些?各有什么优缺点?
答: 垃圾回收算法主要有以下几种:
1. 标记-清除算法(Mark-Sweep)
- 过程:标记所有需要回收的对象,然后统一清除。
- 优点:实现简单。
- 缺点:效率低(标记和清除都慢),产生内存碎片。
- 适用场景:早期垃圾回收器。
2. 复制算法(Copying)
- 过程:将内存分为两半,每次使用一半,存活对象复制到另一半,然后清除原半区。
- 优点:无内存碎片,效率高。
- 缺点:可用内存减半。
- 适用场景:新生代(Eden + Survivor 区采用 8:1:1 比例)。
3. 标记-整理算法(Mark-Compact)
- 过程:标记后移动存活对象到一端,然后清理边界外的内存。
- 优点:无内存碎片。
- 缺点:移动对象开销大,需要更新引用。
- 适用场景:老年代(对象存活率高)。
4. 分代收集算法(Generational Collection)
- 过程:根据对象生命周期将堆分为新生代和老年代,分别采用不同算法(新生代用复制算法,老年代用标记-整理或标记-清除)。
- 优点:结合各算法优点,提高整体效率。
- 缺点:实现复杂。
- 适用场景:现代 JVM 默认策略。
总结:不同算法适用于不同场景,选择合适算法需要在空间、时间和实现复杂度之间权衡。
⭐ 重点:垃圾回收算法是 JVM 核心,面试必问,务必掌握各算法优缺点和适用场景