Skip to content

可达性分析(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)记录标记开始时的引用快照,后续修改不影响本次 GCG1、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)使用,而且现在已经有了更好的替代方案(AutoCloseableCleaner)。

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 收集器标记过程的底层基础。

下一节,我们来看 标记-清除/复制/标记-压缩(对比)

基于 VitePress 构建