一、定义
可达性分析(Reachability Analysis) 是 Java 虚拟机判断堆中对象是否存活的核心算法 ,用来区分存活对象与垃圾对象,为 GC 回收做标记。 核心思路: 以一组特殊起点 GC Roots(根对象) 为源头,顺着引用链向下遍历所有被引用的对象;
- 能通过引用链连接到 GC Roots → 可达,存活,不能回收
- 没有任何引用链连通 GC Roots → 不可达,判定为垃圾,可回收

二、为什么不用「引用计数法」?
早期语言用引用计数(对象被引用 + 1,引用失效 - 1,计数 0 则回收),但有致命缺陷:无法解决循环引用。 示例循环引用:
java
运行
Object a = new Object();
Object b = new Object();
a = b;
b = a;
a = null;
b = null;
- 引用计数法:a、b 互相引用,计数永远不为 0,内存泄漏;
- 可达性分析法:a、b 和 GC Roots 无通路,判定为垃圾正常回收。
三、GC Roots 有哪些(面试必背 6 大类)
GC Roots 是程序运行时一定存活的对象,作为遍历起点:
-
虚拟机栈(栈帧本地变量表)引用的对象 方法局部变量、方法参数;方法执行时栈帧存在,对象存活;方法执行完毕栈帧弹出,引用链断开。
java
运行
public void test(){ User u = new User(); // u是栈局部变量,User对象是GC Roots可达 } -
方法区中静态变量引用的对象
static修饰的类成员,只要类未卸载,对象一直可达。java
运行
class Demo{ static User user = new User(); // 静态引用,GC Root } -
方法区常量池引用的对象 字符串常量池、运行时常量池中的常量对象。
-
本地方法栈(Native JNI)引用的对象 C/C++ 本地方法通过 JNI 引用的 Java 对象。
-
JVM 内部自带引用对象 Class 对象、常驻异常、系统类加载器。
-
被
synchronized同步锁持有对象 正在执行同步代码块、持有锁的对象。
四、完整执行流程
- STW(Stop-The-World)暂停所有用户线程 必须冻结业务线程,防止遍历过程中对象引用被修改,保证引用链快照一致性。
- 标记 GC Roots,识别所有根对象。
- 深度 / 广度优先遍历对象图 从 GC Roots 出发,逐层标记所有直接、间接引用的存活对象(三色标记实现)。
- 遍历结束后 :
- 被标记:存活,保留;
- 未被标记(白色对象):不可达垃圾,后续执行清除。
五、底层实现:三色标记法
可达性分析遍历对象时,将对象分为 3 种颜色,简化标记逻辑、支持并发标记:
- 白色:初始状态,未扫描;遍历结束仍为白色 = 垃圾。
- 灰色 :已扫描到当前对象,但该对象内部引用的子对象还未全部扫描。
- 黑色:当前对象已扫描,且它所有引用的子对象全部标记完成,完全存活。
遍历流程:GC Roots 先标记为灰色,循环取出灰色对象扫描,子对象变白→灰,父对象变黑色,直到无灰色对象。
六、优缺点
优点
- 彻底解决循环引用内存泄漏问题;
- 逻辑清晰,适配分代收集、CMS/G1/ZGC 等主流垃圾收集器。
缺点
- 基础标记阶段需要 STW,会造成业务线程停顿;
- 并发标记场景会出现漏标、多标问题(通过读写屏障解决)。
七、高频面试问答
1. 可达性分析与引用计数的核心区别?
- 引用计数:靠计数器判断,无法处理循环引用,无 STW;
- 可达性分析:靠 GC Roots 引用链判断,解决循环引用,基础标记阶段需要 STW。
2. 局部变量、静态变量为什么能作为 GC Roots?
局部变量存于虚拟机栈,线程执行时栈帧不会销毁;静态变量绑定类,类生命周期贯穿程序运行,二者天然是程序活跃引用,作为遍历起点。
3. 对象不可达后会立刻被回收吗?
不会。对象被判定不可达后,会进入两次标记流程:
- 第一次标记:筛选重写了
finalize()方法的对象; - 放入 F-Queue 队列,由低优先级线程执行
finalize();若在 finalize 中重新建立 GC Roots 引用链,可自救存活; - 第二次标记:finalize 执行完毕后,无引用链则真正回收。