面试题:JVM 如何判断一个对象可以被回收?
JVM 主要通过**可达性分析(Reachability Analysis)**判断对象是否存活。
需要了解两种方法:
- 引用计数法
- 可达性分析(Java 主流 JVM 使用的核心方法)
引用计数法
基本原理
为每个对象维护一个引用计数器:
- 当对象被引用时,引用计数增加。
- 当引用失效时,引用计数减少。
- 当引用计数为 0 时,对象可以被认为是垃圾。
示例:
Object a = new Object();a = null;第一行创建对象,变量 a 指向该对象。
第二行将 a 设为 null,如果没有其他引用指向该对象,它就可能成为垃圾。
缺点:无法单独解决循环引用
class Node { Node next;}
Node a = new Node();Node b = new Node();
a.next = b;b.next = a;
a = null;b = null;此时:
- 对象 A 引用对象 B。
- 对象 B 引用对象 A。
- 外部变量
a、b已经不再引用这两个对象。 - 两个对象之间仍然存在相互引用。
引用计数法可能认为它们仍然存活,因为引用计数没有归零。
结论:引用计数法无法单独解决循环引用问题,不是 Java 主流 JVM 判断对象存活的主要方法。
可达性分析(重点)
基本原理
可达性分析从一组称为 GC Roots 的对象出发,沿着引用关系进行遍历。
判断规则:
- 能够从 GC Roots 到达的对象:暂时认为存活。
- 无法从任何 GC Roots 到达的对象:被判定为不可达,成为垃圾回收的候选对象。
注意:不可达不代表对象一定会立即被回收。
示例
Object a = new Object();Object b = a;
a = null;虽然 a 已经变成 null,但 b 仍然引用着原来的对象。
如果 b 仍然是 GC Roots 可达的局部变量引用,那么原对象仍然可达,不能仅仅因为 a = null 就认定它是垃圾。
判断对象能否被回收,关键是是否还存在从 GC Roots 到达它的引用路径,而不是某个变量是否为 null。
引用关系示意
GC Roots | v对象 A | v对象 B
对象 C(没有从 GC Roots 出发的引用路径)- 对象 A 可达,因此存活。
- 对象 B 虽然没有被局部变量直接引用,但可以通过对象 A 到达,因此也存活。
- 对象 C 不可达,因此可能被回收。
常见的 GC Roots
面试中需要掌握以下几类:
- Java 虚拟机栈中的引用:例如当前方法中的局部变量引用。
- 静态字段引用的对象:例如类的静态变量持有的对象。
- JNI 引用:Java 调用本地代码时,本地代码持有的相关引用。
- JVM 内部引用:例如某些运行时数据结构持有的对象。
注意:静态变量引用的对象并非永远不能回收。如果静态字段被清空,且对象没有其他可达路径,它仍可能成为垃圾。
面试回答模板
问:JVM 如何判断一个对象可以被回收?
答:
JVM 主要通过可达性分析判断对象是否存活。它从 GC Roots 出发,沿着对象引用关系进行遍历。如果一个对象无法从任何 GC Roots 到达,就会被判定为不可达对象,成为垃圾回收的候选对象。
常见的 GC Roots 包括虚拟机栈中的引用、静态字段引用、JNI 引用以及 JVM 内部引用。
另外,引用计数法通过统计对象的引用数量判断对象是否存活,但无法单独解决循环引用问题,因此不是 Java 主流 JVM 判断对象存活的主要方法。
需要记住的重点
- JVM 判断对象是否存活的核心方法:可达性分析。
- 可达性分析的起点:GC Roots。
- 能从 GC Roots 到达的对象:暂时认为存活。
- 无法从 GC Roots 到达的对象:不可达,成为垃圾回收的候选对象。
- 引用计数法的主要缺点:无法单独解决循环引用问题。
- 对象不可达不代表一定立即回收。
自测题
题目 1
Object a = new Object();Object b = a;
a = null;假设 b 仍然是 GC Roots 可达的局部变量引用,原对象能否被回收?
答案:不能被当作垃圾回收,因为 b 仍然引用着原对象。
题目 2
两个对象互相引用,但没有任何 GC Roots 能够到达它们,这两个对象能否被回收?
答案:可以成为垃圾回收的候选对象。可达性分析能够识别这种循环引用导致的不可达对象。
题目 3
对象不可达后,JVM 是否一定立即回收它?
答案:不一定。不可达意味着它成为垃圾回收的候选对象,实际回收时间由 JVM 的垃圾回收机制决定。