可达性分析(GC Roots/finalization 机制)
可达性分析:JVM 的垃圾判定方法
与引用计数法不同,JVM 使用可达性分析(Reachability Analysis)来判定对象是否是垃圾。
核心思想很简单:从「根节点」出发,能顺着引用链找到的对象就是存活对象,找不到的就是垃圾。
GC Roots:垃圾回收的「根」
GC Roots 是一组特殊的对象,它们天然是存活的,其他所有存活对象都直接或间接被 GC Roots 引用。
GC Roots 的组成
java
public class GCRootsDemo {
// 1. 虚拟机栈(栈帧中的本地变量表)
Object localVar = new Object();
public static void main(String[] args) {
// 2. 方法区中的类静态属性
static Object staticObj = new Object();
// 3. 方法区中的常量(字符串常量池)
String interned = "hello";
// 4. 本地方法栈中的 JNI 引用
// native 方法持有的对象引用
// 5. 活跃的线程
Thread thread = Thread.currentThread();
// 6. 运行中的协程/虚拟线程
// JDK 21+ VirtualThread
// 7. 监视器锁(synchronized 持有的对象)
synchronized (new Object()) {
// 进入同步块的 Object 是 GCRoot
}
// 8. JVM 内部数据结构(如 Class 对象)
}
}GC Roots 的完整列表
| GC Roots 类型 | 说明 | 示例 |
|---|---|---|
| 虚拟机栈中的本地变量 | 正在执行的方法的局部变量 | 方法参数、本地变量 |
| 方法区中的静态变量 | static 修饰的字段 | static Object obj |
| 方法区中的常量 | 字符串常量池中的字符串 | String s = "hello" |
| 本地方法栈中的 JNI 引用 | native 方法持有的对象引用 | JNI 代码中的 jobject |
| 活跃的线程 | 正在运行的线程 | Thread.currentThread() |
| 监视器锁 | 被 synchronized 持有的对象 | synchronized(obj) 中的 obj |
| JVM 内部数据结构 | Class 对象、ClassLoader 等 | Class<?> clazz |
| JVMTI 强引用 | 调试器/Profiler 强持有的对象 | - |
可达性分析的执行过程
java
public class ReachabilityDemo {
static class Node {
Node next;
Node(String name) { }
}
public static void main(String[] args) {
// 创建对象链:A → B → C → D
Node A = new Node("A");
Node B = new Node("B");
Node C = new Node("C");
Node D = new Node("D");
A.next = B;
B.next = C;
C.next = D;
// 现在:GC Roots → A → B → C → D(全部可达)
// 切断 A → B
A = null; // A 不再是 GC Roots
// 现在:B → C → D(通过 GC Roots 无法到达 B、C、D)
// B、C、D 成为垃圾!
}
}三色标记:可达性分析的算法实现
JVM 使用三色标记(Tri-color Marking)算法来实现可达性分析:
三色标记:
├── 白色(White) ──→ 对象未被访问过
├── 灰色(Gray) ──→ 对象被访问过,但引用还未全部扫描
└── 黑色(Black) ──→ 对象和其引用全部扫描完成标记过程
初始状态(全部白色):
GC Roots ──→ [A] ──→ [B] ──→ [C] ──→ [D]
(灰色) (白色) (白色) (白色) (白色)
步骤1:GC Roots 被标记为灰色
GC Roots ──→ [A] ──→ [B] ──→ [C] ──→ [D]
(黑色) (灰色) (白色) (白色) (白色)
步骤2:A 被扫描,其引用 B 被标记为灰色
GC Roots ──→ [A] ──→ [B] ──→ [C] ──→ [D]
(黑色) (黑色) (灰色) (白色) (白色)
步骤3:B 被扫描,其引用 C 被标记为灰色
GC Roots ──→ [A] ──→ [B] ──→ [C] ──→ [D]
(黑色) (黑色) (黑色) (灰色) (白色)
步骤4:C 被扫描,其引用 D 被标记为灰色
GC Roots ──→ [A] ──→ [B] ──→ [C] ──→ [D]
(黑色) (黑色) (黑色) (黑色) (灰色)
步骤5:D 被扫描,没有更多引用,标记为黑色
GC Roots ──→ [A] ──→ [B] ──→ [C] ──→ [D]
(黑色) (黑色) (黑色) (黑色) (黑色)
标记完成!白色对象(D 的下一个,如果存在)就是垃圾增量更新与原始快照
并发标记的问题
在并发 GC 中,GC 线程和应用线程同时运行。如果应用线程在标记过程中修改了引用关系,会出现对象消失的问题:
场景:应用线程在标记过程中修改引用
GC 线程标记中:
GC Roots ──→ [A] ──→ [B]
↑
应用线程修改:A.next = null; B.next = C
如果标记过程中 B 已经扫描过,但 A 还没扫描到:
→ C 可能被误判为垃圾(因为没有灰色对象引用它)
→ 对象消失问题解决方案
| 方案 | 原理 | 代表收集器 |
|---|---|---|
| 增量更新(Incremental Update) | 把被修改的对象(A)重新标记为灰色 | CMS(G1 之前) |
| 原始快照(SATB,Snapshot At The Beginning) | 记录标记开始时的引用快照,后续修改不影响本次 GC | G1、ZGC |
finalize():对象的最后一次机会
Object.finalize() 是对象被 GC 回收前最后一次「自救」的机会。
怎么工作
java
public class FinalizeDemo {
static class Survivable {
static Survivable instance; // 类变量,GC Root
@Override
protected void finalize() throws Throwable {
// 对象即将被回收时,JVM 会调用这个方法
System.out.println("finalize() 被调用了!");
instance = this; // 把自己赋值给类变量,逃脱回收
}
}
public static void main(String[] args) throws InterruptedException {
instance = new Survivable();
// 取消引用
instance = null;
// 触发 GC
System.gc();
Thread.sleep(1000);
// 如果 finalize() 中把自己赋值给了 instance
// 这个对象就被「救」回来了
if (instance != null) {
System.out.println("对象逃脱了 GC!");
}
}
}finalize() 的问题
finalize() 有很多严重的问题:
| 问题 | 说明 |
|---|---|
| 不确定性 | GC 发生时间不确定,finalize() 可能永不执行 |
| 性能开销 | finalize() 的对象需要至少两次 GC 才能回收 |
| 继承问题 | 子类如果没有重写 finalize(),开销不必要 |
| 线程安全 | finalize() 在专门的 Finalizer 线程执行,有线程安全问题 |
结论:永远不要使用 finalize()。它的唯一合法用途是配合本地资源(native resource)使用,而且现在已经有了更好的替代方案(AutoCloseable、Cleaner)。
Cleaner:finalize() 的替代品
JDK 9 废弃了 finalize(),推荐使用 java.lang.ref.Cleaner:
java
public class CleanerDemo implements AutoCloseable {
private static Cleaner cleaner = Cleaner.create();
private final byte[] nativeResource; // 本地资源的包装
public CleanerDemo() {
this.nativeResource = new byte[1024];
// 注册清理任务
cleaner.register(this, () -> {
// 在 GC 时执行清理逻辑
System.out.println("清理本地资源");
});
}
@Override
public void close() {
// 手动清理(推荐)
}
}本节小结
可达性分析的核心要点:
| 关键点 | 说明 |
|---|---|
| GC Roots | 天然存活的对象集合,垃圾判定起点 |
| 可达性分析 | 从 GC Roots 出发,顺着引用链找到存活对象 |
| 三色标记 | 白色(未访问)、灰色(已访问未扫描完)、黑色(已扫描完) |
| 并发问题 | 对象消失问题(增量更新 / SATB 解决) |
| finalize() | 已被废弃,使用 Cleaner 替代 |
理解可达性分析,是理解所有现代 GC 收集器标记过程的底层基础。
下一节,我们来看 标记-清除/复制/标记-压缩(对比)。
