JVM 如何判断对象是否存活的?

#Java#面试八股

JVM 如何判断对象是否存活的?

结论先行:HotSpot 用**可达性分析(reachability analysis)**判断对象存活:从一组称为 GC Roots 的根对象出发,沿引用链遍历,能被遍历到的对象存活,遍历不到的对象可回收。引用计数(reference counting)虽然直观,但解决不了循环引用,还要为每次引用赋值付出原子开销,因此商用 JVM 不用它。

一、两种判定方式对比

维度引用计数可达性分析
基本思路每个对象维护被引用次数,减到 0 即死从 GC Roots 遍历,不可达即死
循环引用无法识别,互相引用永不归零天然解决,环上没有根就整体回收
额外开销每次引用赋值都要增减,需保证原子性只在 GC 时遍历,平时零开销
实现位置编译器插桩或对象头字段收集器的标记阶段
典型代表Python、Swift、早期 Objective-CHotSpot 全系收集器
GC Roots
  ├── 线程栈局部变量 A ──→ B ──→ C        (A、B、C 存活)
  ├── 静态字段 D ──→ E                      (D、E 存活)
  └── 常量 / JNI 引用 / 同步锁 ...

孤立对象环:F ──→ G ──→ F
  没有任何根指向 F,整环一起回收;引用计数法会认为两者计数都为 1

二、谁能当 GC Roots

根类型说明
虚拟机栈中局部变量表引用的对象方法参数、局部变量、临时值
方法区中静态属性引用的对象static 字段
方法区中常量引用的对象字符串常量池、static final
本地方法栈 JNI 引用的对象native 方法持有的全局引用
被 synchronized 持有的对象监视器(monitor)关联的对象
JVM 内部引用系统类加载器、基本类型包装类缓存、常驻异常对象

三、一次完整的判定不是「一次标记」

对象被判定不可达后并不立即消失,而是走「两次标记」:

  1. 第一次标记:可达性分析后发现不可达,检查是否需要执行 finalize()——未覆盖或已执行过则直接回收。
  2. 若需要执行,对象被放进 F=Queue,由低优先级线程触发 finalize()。
  3. 第二次标记:finalize() 中若对象重新与引用链上的对象建立关联,则移出队列逃过一劫。
public class FinalizeEscape {
    private static FinalizeEscape SAVE_HOOK;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        SAVE_HOOK = this;                      // 把自己挂回静态字段,完成自救
        System.out.println("finalize 执行,对象自救成功");
    }

    public static void main(String[] args) throws Exception {
        SAVE_HOOK = new FinalizeEscape();
        SAVE_HOOK = null;                      // 断开根引用
        System.gc();
        Thread.sleep(500);                     // 等 finalize 线程跑完
        System.out.println("第一次: " + (SAVE_HOOK != null ? "存活" : "已死"));

        SAVE_HOOK = null;                      // 再断一次
        System.gc();
        Thread.sleep(500);
        System.out.println("第二次: " + (SAVE_HOOK != null ? "存活" : "已死"));
    }
}
// 输出:
// finalize 执行,对象自救成功
// 第一次: 存活
// 第二次: 已死(finalize 只执行一次)

finalize() 在 JDK 9 起被标记为 deprecated,JDK 18 起默认禁用;替代方案是 Cleaner 或 try-with-resources。

四、三色标记:并发场景下的可达性分析

并发收集器在应用线程运行时标记,用三种颜色描述状态:

颜色含义
白色尚未访问,最终仍是白色即回收
灰色自身已访问,成员未扫完
黑色自身与成员都扫完

并发标记的漏标只会在两个条件同时成立时发生:赋值器删掉了灰色对象到白色对象的引用(多标),又让黑色对象新增了指向该白色对象的引用(少标)。解决办法两条路:**增量更新(incremental update)**记录黑色对象新增的引用,重新标记时再扫一遍;**原始快照(SATB)**记录删除的旧引用,按快照继续标记。

# 观察三色标记的并发细节(需 debug 版 JVM)
java -Xlog:gc+marking=debug -XX:+UseG1GC -jar app.jar

五、常见追问

追问回答要点
引用计数为什么被淘汰循环引用、无法处理、每次赋值都要原子更新,吞吐还不如遍历一次
可达性分析会 STW 吗根节点枚举必须 STW,其余标记阶段可并发;OopMap + 安全点让根枚举足够快
不可达就一定被回收吗不一定,软引用、弱引用、finalize() 自救、Cleaner 都会影响最终结果
为什么还要三色标记并发标记必须能表达「扫描到一半」的中间状态,否则无法增量推进
栈上局部变量出作用域后立刻回收吗不保证,只要局部变量表槽位仍持有引用,对象就还是 GC Root 可达

小结

判断存活靠「从 GC Roots 出发的可达性分析」,引用计数只作为对照概念出现。真正的工程难点在并发标记的正确性,三色标记加读/写屏障才是现代收集器的核心。

笔记加载中…