垃圾回收
2025/12/12大约 6 分钟
垃圾回收
垃圾回收(Garbage Collection,GC)是 JVM 自动管理内存的机制,负责回收不再使用的对象所占用的内存。
如何判断对象可回收
引用计数法
给对象添加一个引用计数器,每当有引用指向它时加 1,引用失效时减 1,计数为 0 时可回收。
// 引用计数法的问题:循环引用
class Node {
Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b; // a 引用 b
b.next = a; // b 引用 a
a = null;
b = null;
// a 和 b 相互引用,计数都不为 0,但都无法访问问题
引用计数法无法解决循环引用问题,JVM 不使用此方法。
可达性分析
从 GC Roots 出发,向下搜索,搜索走过的路径称为引用链。如果一个对象到 GC Roots 没有任何引用链相连,则该对象可回收。
GC Roots
│
├── Object 1 ──→ Object 2
│ │
│ └────→ Object 3
│
└── Object 4
Object 5 ──→ Object 6 ← 不可达,可回收GC Roots
可以作为 GC Roots 的对象:
| 类型 | 说明 |
|---|---|
| 虚拟机栈中的引用 | 栈帧中的局部变量表引用的对象 |
| 方法区中的静态属性 | 类的静态变量引用的对象 |
| 方法区中的常量 | 常量池中引用的对象 |
| Native 方法引用的对象 | JNI 引用的对象 |
| 同步锁持有的对象 | synchronized 锁住的对象 |
引用类型
JDK 1.2 后,Java 将引用分为四种类型:
强引用(Strong Reference)
最常见的引用,只要强引用存在,对象就不会被回收。
Object obj = new Object(); // 强引用
obj = null; // 断开引用后可回收软引用(Soft Reference)
内存不足时才会被回收,常用于缓存。
import java.lang.ref.SoftReference;
Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);
obj = null;
// 获取对象
Object o = softRef.get(); // 可能返回 null弱引用(Weak Reference)
下次 GC 时一定会被回收。
import java.lang.ref.WeakReference;
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
obj = null;
System.gc();
Object o = weakRef.get(); // null虚引用(Phantom Reference)
最弱的引用,无法通过虚引用获取对象,主要用于跟踪对象被回收的状态。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
obj = null;
phantomRef.get(); // 始终返回 null引用类型对比
| 引用类型 | 回收时机 | 使用场景 |
|---|---|---|
| 强引用 | 从不回收 | 普通引用 |
| 软引用 | 内存不足时 | 缓存 |
| 弱引用 | 下次 GC | WeakHashMap |
| 虚引用 | 任何时候 | 跟踪回收状态 |
垃圾回收算法
标记-清除(Mark-Sweep)
标记阶段:标记所有可达对象
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ ✓ │ │ ✓ │ │ ✓ │ │ │ ✓ │
└───┴───┴───┴───┴───┴───┴───┴───┘
清除阶段:回收未标记的对象
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ ✓ │ │ ✓ │ │ ✓ │ │ │ ✓ │
└───┴───┴───┴───┴───┴───┴───┴───┘
问题:产生内存碎片
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ │ B │ │ C │ │ │ D │
└───┴───┴───┴───┴───┴───┴───┴───┘优点:实现简单
缺点:产生内存碎片
复制算法(Copying)
将内存分为两块,每次只使用一块,GC 时将存活对象复制到另一块。
使用中:
┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐
│ A │ │ B │ C │ │ │ │ │ │
└───┴───┴───┴───┘ └───┴───┴───┴───┘
From To
GC 后:
┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐
│ │ │ │ │ │ A │ B │ C │ │
└───┴───┴───┴───┘ └───┴───┴───┴───┘
From To优点:无内存碎片,效率高
缺点:内存利用率只有 50%
标记-整理(Mark-Compact)
标记后将存活对象向一端移动,然后清理边界外的内存。
标记阶段:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ │ B │ │ C │ │ │ D │
└───┴───┴───┴───┴───┴───┴───┴───┘
整理阶段:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ A │ B │ C │ D │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┘优点:无内存碎片,内存利用率高
缺点:移动对象有开销
分代收集
根据对象存活周期不同,使用不同的算法。
┌─────────────────────────────────────────────────────┐
│ 年轻代 │
│ 使用复制算法(对象存活率低,复制成本小) │
│ ┌────────────┐ ┌─────┐ ┌─────┐ │
│ │ Eden │ │ S0 │ │ S1 │ │
│ └────────────┘ └─────┘ └─────┘ │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 老年代 │
│ 使用标记-清除或标记-整理(对象存活率高) │
└─────────────────────────────────────────────────────┘垃圾收集器
新生代收集器
Serial 收集器
单线程收集器,简单高效,适合单核 CPU。
用户线程 ────────────┬─────────┬────────────────
│ 暂停 │
GC 线程 │ ═══════ │
│ Serial │-XX:+UseSerialGCParNew 收集器
Serial 的多线程版本,常与 CMS 配合使用。
用户线程 ────────────┬─────────┬────────────────
│ 暂停 │
GC 线程 1 │ ═══════ │
GC 线程 2 │ ═══════ │
GC 线程 3 │ ═══════ │-XX:+UseParNewGCParallel Scavenge 收集器
关注吞吐量,适合后台运算任务。
-XX:+UseParallelGC
-XX:MaxGCPauseMillis=100 # 最大停顿时间
-XX:GCTimeRatio=99 # 吞吐量(1/(1+99)=1%)老年代收集器
Serial Old 收集器
Serial 的老年代版本,使用标记-整理算法。
Parallel Old 收集器
Parallel Scavenge 的老年代版本。
-XX:+UseParallelOldGCCMS 收集器
以最短回收停顿时间为目标,使用标记-清除算法。
用户线程 ─────┬────┬─────────────┬────┬─────
│初始│ │重新│
│标记│ │标记│
GC 线程 │ │ 并发标记 │ │ 并发清除
└─────────────┘ └──────四个阶段:
- 初始标记:STW,标记 GC Roots 直接关联的对象
- 并发标记:与用户线程并发,标记可达对象
- 重新标记:STW,修正并发标记期间的变动
- 并发清除:与用户线程并发,清除垃圾
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75 # 老年代使用率达到 75% 触发CMS 缺点:
- CPU 敏感
- 产生浮动垃圾
- 内存碎片
G1 收集器(JDK 9 默认)
面向服务端,低延迟、可预测的停顿时间。
Region 划分
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ E │ E │ S │ O │ O │ H │ O │ E │
├───┼───┼───┼───┼───┼───┼───┼───┤
│ E │ O │ O │ E │ S │ O │ O │ E │
├───┼───┼───┼───┼───┼───┼───┼───┤
│ O │ O │ E │ E │ E │ O │ H │ O │
└───┴───┴───┴───┴───┴───┴───┴───┘
E = Eden S = Survivor O = Old H = Humongous工作流程
- 初始标记:STW,标记 GC Roots 直接关联的对象
- 并发标记:与用户线程并发
- 最终标记:STW,处理并发阶段遗留的 SATB 记录
- 筛选回收:STW,按回收价值排序,回收部分 Region
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 期望最大停顿时间
-XX:G1HeapRegionSize=2m # Region 大小ZGC(JDK 11+)
超低延迟收集器,停顿时间不超过 10ms。
-XX:+UseZGC收集器对比
| 收集器 | 新生代/老年代 | 算法 | 线程 | 特点 |
|---|---|---|---|---|
| Serial | 新生代 | 复制 | 单线程 | 简单高效 |
| ParNew | 新生代 | 复制 | 多线程 | 配合 CMS |
| Parallel Scavenge | 新生代 | 复制 | 多线程 | 吞吐量优先 |
| Serial Old | 老年代 | 标记-整理 | 单线程 | |
| Parallel Old | 老年代 | 标记-整理 | 多线程 | 吞吐量优先 |
| CMS | 老年代 | 标记-清除 | 并发 | 低延迟 |
| G1 | 全堆 | 标记-整理+复制 | 并发 | 可控停顿 |
| ZGC | 全堆 | 标记-整理 | 并发 | 超低延迟 |
GC 类型
| 类型 | 说明 |
|---|---|
| Minor GC / Young GC | 新生代 GC |
| Major GC / Old GC | 老年代 GC |
| Full GC | 整堆 GC(包括方法区) |
触发条件
Minor GC:Eden 区满
Full GC:
- 老年代空间不足
- 方法区空间不足
System.gc()调用- Minor GC 后存活对象大于老年代剩余空间
小结
- 可达性分析:从 GC Roots 出发,不可达的对象可回收
- 引用类型:强 > 软 > 弱 > 虚,影响回收时机
- 算法:标记-清除、复制、标记-整理、分代收集
- 收集器:Serial、ParNew、Parallel、CMS、G1、ZGC
- 选择建议:
- 小内存、单核:Serial
- 吞吐量优先:Parallel
- 低延迟:CMS、G1
- 超低延迟:ZGC
