垃圾回收算法(GC 算法):定义 + 核心原理 + 常见类型 + 示例代码
一、什么是垃圾回收算法?
垃圾回收(Garbage Collection,GC)算法,是 JVM(Java 虚拟机)用于自动识别并回收 "无用对象"(垃圾)所占用内存的核心机制 ------ 简单说,就是 JVM "自动清理内存垃圾" 的一套规则和步骤。
核心目标:
- 识别垃圾:判断哪些对象是 "无用的"(程序后续不会再使用);
- 回收内存:释放无用对象占用的内存,避免内存泄漏;
- 优化性能:在 "回收效率""内存利用率""停顿时间" 之间做平衡(比如减少 GC 时程序卡顿的时间)。
前置:如何定义 "垃圾"?(垃圾识别的 2 种核心方式)
GC 算法的第一步是 "找垃圾",JVM 主要通过以下 2 种方式判断对象是否为垃圾:
- 引用计数法 :给每个对象加一个 "引用计数器",有引用指向对象时计数器 + 1,引用失效时 - 1;计数器为 0 则认为是垃圾。
- 优点:简单高效,无停顿;
- 缺点:无法解决 "循环引用"(A 引用 B,B 引用 A,两者都无其他引用,但计数器不为 0,无法回收)。
- 可达性分析算法(JVM 主流选择) :以 "GC Roots" 为起点,遍历对象引用链;能被 GC Roots 直接或间接引用的对象是 "存活对象",反之则是垃圾。
- 常见 GC Roots:虚拟机栈中局部变量表的引用、方法区中类静态属性的引用、常量池的引用、本地方法栈中 JNI 的引用等;
- 优点:能解决循环引用问题,是 JVM 判断垃圾的核心标准。
二、常见垃圾回收算法(原理 + 优缺点)
JVM 发展至今,衍生出多种 GC 算法,核心分为 "基础算法" 和 "分代收集算法"(基于基础算法的优化组合),以下是最核心的 4 种:
1. 标记 - 清除算法(Mark-Sweep):最基础的 GC 算法
核心原理(两步走):
- 标记阶段:通过可达性分析,标记所有存活对象(比如给存活对象打一个 "存活标记");
- 清除阶段:遍历整个堆内存,回收所有未被标记的垃圾对象(释放内存,将空闲内存加入 "空闲链表")。
通俗类比:
房间里(堆内存)有很多物品(对象),先标记出 "还要用的物品"(存活对象),再把没标记的 "无用物品"(垃圾)全部扔掉,剩下的空闲空间记录下来。
优缺点:
- 优点:实现简单,无需移动对象;
- 缺点:
- 内存碎片:回收后空闲内存会分散成多个不连续的 "碎片"(比如回收了 2 个 100KB 的对象,可能形成两个独立的 100KB 碎片,无法满足 200KB 的连续内存需求);
- 效率较低:需要遍历整个堆两次(标记一次、清除一次),堆内存越大,停顿时间越长。
适用场景:
少量垃圾、对内存连续性要求不高的场景(早期 JVM 曾用,现在很少单独使用)。
2. 复制算法(Copying):解决内存碎片的高效算法
核心原理(三步):
- 划分区域:将堆内存划分为两个大小相等的区域(From 区、To 区),每次只使用其中一个区域(From 区);
- 标记复制:通过可达性分析标记 From 区的存活对象,将所有存活对象 "复制" 到 To 区(按顺序排列,无碎片);
- 交换角色:清空 From 区,将 From 区和 To 区的角色互换(下次 GC 时,原来的 To 区变为 From 区,原来的 From 区变为 To 区)。
通俗类比:
把房间分成 A、B 两个相同大小的区域,平时只在 A 区放物品;清理时,把 A 区 "还要用的物品" 按顺序搬到 B 区,然后把 A 区清空,下次就用 B 区,循环往复。
优缺点:
- 优点:
- 无内存碎片:存活对象复制后按顺序排列,空闲内存连续;
- 效率高:只遍历存活对象(复制 + 标记一次完成),垃圾越多,效率越高(因为复制的对象少);
- 缺点:
- 内存利用率低:堆内存只能使用一半(另一半长期空闲);
- 复制开销:存活对象较多时,复制操作的开销会增大。
适用场景:
存活对象少的区域(比如 JVM 的 "新生代",大部分对象都是 "朝生夕死",存活时间短,复制开销小)。
3. 标记 - 整理算法(Mark-Compact):平衡碎片与内存利用率
核心原理(三步):
- 标记阶段:与 "标记 - 清除算法" 一致,通过可达性分析标记所有存活对象;
- 整理阶段:将所有存活对象 "向堆内存的一端移动"(按顺序排列);
- 清除阶段:直接清理掉存活对象边界之外的所有垃圾对象(释放连续的空闲内存)。
通俗类比:
先标记房间里 "还要用的物品",然后把所有有用的物品都搬到房间的一端按顺序放好,最后把房间另一端的无用物品全部扔掉 ------ 空闲空间集中在一端,无碎片。
优缺点:
- 优点:
- 无内存碎片:存活对象整理后连续排列;
- 内存利用率高:无需划分两个区域,堆内存全部可用;
- 缺点:
- 效率较低:比标记 - 清除算法多了 "整理(移动对象)" 步骤,移动对象时还需要更新对象的引用地址(比如 A 对象引用 B 对象,B 对象移动后,A 对象的引用地址要同步修改);
- 停顿时间长:移动对象 + 更新引用的开销较大,堆内存越大,停顿越明显。
适用场景:
存活对象多的区域(比如 JVM 的 "老年代",对象存活时间长,垃圾少,整理开销相对可控)。
4. 分代收集算法(Generational Collection):JVM 的主流选择(组合优化)
核心原理(基于 "对象生命周期差异" 的优化):
JVM 根据对象的 "存活时间",将堆内存划分为 新生代(Young Generation) 和 老年代(Old Generation)(部分 JVM 还有永久代 / 元空间,不参与 GC),不同区域采用不同的 GC 算法:
- 新生代:对象存活时间短(朝生夕死,比如临时变量),存活对象少 → 采用 "复制算法"(效率高、无碎片);
- 新生代进一步划分为:Eden 区(80%)、Survivor0 区(10%)、Survivor1 区(10%)(而非等大的 From/To 区,优化内存利用率);
- 流程:对象先分配到 Eden 区,Eden 区满后触发 Minor GC(新生代 GC),将 Eden 区和 Survivor0 区的存活对象复制到 Survivor1 区,清空 Eden 和 Survivor0;下次 Minor GC 时,Survivor0 和 Survivor1 角色互换;对象在 Survivor 区经历多次 GC 后仍存活,会被 "晋升" 到老年代。
- 老年代:对象存活时间长(比如缓存对象、单例对象),存活对象多 → 采用 "标记 - 整理算法"(平衡碎片与内存利用率);
- Full GC:当老年代满时,会触发 Full GC(全局 GC),同时回收新生代和老年代的垃圾(停顿时间较长,应尽量避免)。
通俗类比:
把房间分成 "临时储物区"(新生代)和 "长期储物区"(老年代);
- 临时储物区:放常用但可能很快丢弃的物品,用 "复制法" 清理(效率高);
- 长期储物区:放长期使用的物品,用 "标记 - 整理法" 清理(无碎片,利用率高)。
核心优势:
结合了 "复制算法" 和 "标记 - 整理算法" 的优点,根据不同区域的对象特性选择最优算法,兼顾 "效率""内存利用率" 和 "低碎片",是目前所有商业 JVM(如 HotSpot)的默认 GC 算法框架。
三、示例代码(观察 GC 行为,验证核心逻辑)
GC 算法是 JVM 底层实现(C/C++ 编写),Java 无法直接编写 GC 算法本身,但可以通过代码 观察 GC 触发、垃圾回收过程、分代晋升机制,以下是 3 个实用示例:
示例 1:验证 "可达性分析"------ 不可达对象被回收
通过切断对象与 GC Roots 的引用,观察对象被 GC 回收的过程(利用 finalize() 感知回收,仅用于测试)。
java
public class GCDemo1 {
// 标记对象是否被回收
private static boolean isRecycled = false;
// 垃圾回收前会调用该方法(仅一次,实际开发禁止使用)
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("【GC 触发】当前对象被回收!");
isRecycled = true;
}
public static void main(String[] args) throws InterruptedException {
// 1. 创建对象(obj 是 GC Roots,对象可达)
GCDemo1 obj = new GCDemo1();
System.out.println("对象创建后,是否被回收:" + isRecycled); // false
// 2. 切断引用(对象不可达,成为垃圾)
obj = null;
// 3. 建议 JVM 执行 GC(System.gc() 是建议,非强制)
System.gc();
// 暂停主线程,给 GC 执行时间
Thread.sleep(1000);
// 4. 验证回收结果
System.out.println("触发 GC 后,是否被回收:" + isRecycled); // true
}
}
java
对象创建后,是否被回收:false
【GC 触发】当前对象被回收!
触发 GC 后,是否被回收:true
关键说明:
obj = null切断了对象与 GC Roots 的引用,对象成为垃圾;finalize()仅用于测试观察,实际开发中禁止依赖(不可靠、效率低)。
示例 2:观察 "新生代 Minor GC"------ 朝生夕死的对象
新生代对象 "朝生夕死" 会频繁触发 Minor GC,通过创建大量临时对象,结合 JVM 参数打印 GC 日志观察。
java
运行
java
public class GCDemo2 {
public static void main(String[] args) {
// 循环创建大量临时对象(使用后立即不可达)
for (int i = 0; i < 1000000; i++) {
// 匿名对象无引用,直接成为垃圾
new Object() {};
}
System.out.println("循环结束,已触发多次 Minor GC");
}
}
运行方式(添加 JVM 参数打印 GC 日志):
- IDEA:Run → Edit Configurations → VM options 输入:
java
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
命令行:
java
java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps GCDemo2.java
核心 GC 日志解读(片段):
java
0.092: [GC (Allocation Failure) [PSYoungGen: 65536K->8192K(76288K)] 65536K->8200K(251392K), 0.004ms]
GC (Allocation Failure):新生代 Eden 区满,触发 Minor GC;PSYoungGen:新生代使用的收集器(Parallel Scavenge);65536K->8192K:新生代 GC 前使用 65536K,GC 后仅存 8192K 存活对象,说明大量临时对象被回收。
示例 3:验证 "老年代晋升"------ 长期存活对象进入老年代
对象在新生代经历多次 Minor GC 后仍存活,会晋升到老年代。通过保留部分对象引用,观察老年代内存变化。
java
import java.util.ArrayList;
import java.util.List;
public class GCDemo3 {
// 持有对象引用,避免被回收(让对象长期存活)
private static List<Object> survivorList = new ArrayList<>();
public static void main(String[] args) {
// 循环创建对象,部分对象存入集合(长期存活)
for (int i = 0; i < 10000; i++) {
// 每个对象 10KB
byte[] obj = new byte[1024 * 10];
// 每 10 个对象保留一个,让其长期存活
if (i % 10 == 0) {
survivorList.add(obj);
}
}
System.out.println("创建完成,部分对象已晋升到老年代");
}
}
运行 JVM 参数(限制堆区域大小,便于观察晋升):
java
-XX:+PrintGCDetails -XX:NewSize=100m -XX:MaxNewSize=100m -XX:OldSize=200m -XX:MaxOldSize=200m
核心 GC 日志解读(片段):
java
0.158: [Full GC (Ergonomics) [PSYoungGen: 98304K->0K(98304K)] [ParOldGen: 10240K->19840K(196608K)] 108544K->19840K(294912K), 0.015ms]
Full GC:老年代内存不足触发全局 GC;ParOldGen: 10240K->19840K:老年代内存使用量增加,说明长期存活的对象成功晋升。
四、核心总结
- 垃圾回收算法的本质:解决 "如何高效识别垃圾 + 回收内存" 的问题,核心权衡 "效率、内存利用率、内存碎片";
- 基础算法对比:
- 标记 - 清除:简单但有碎片、效率低;
- 复制算法:高效无碎片但内存利用率低;
- 标记 - 整理:无碎片利用率高但效率低;
- JVM 的选择:采用 "分代收集算法"(组合优化),新生代用复制算法,老年代用标记 - 整理算法;
- 关键记忆点:对象 "朝生夕死" 用复制,"长命百岁" 用标记 - 整理,分代收集是主流。
简单记:基础算法各有优劣,分代收集取长补短 ------ 这是 GC 算法的核心设计思路。