面试题:哪些对象可以作为 GC Roots?
GC Roots 是可达性分析的起点。
JVM 从 GC Roots 出发,沿着对象之间的引用关系进行遍历,判断对象是否可达。如果一个对象无法从任何 GC Roots 到达,就会成为垃圾回收的候选对象。
常见的 GC Roots
1. 虚拟机栈中的引用
例如方法中的局部变量引用。
public void test() { Object obj = new Object();}当 test() 方法执行时,局部变量 obj 引用着一个对象。如果该引用在当前执行状态下仍然有效,并且能够从 GC Roots 到达这个对象,那么该对象就不会因为没有其他引用而被判定为垃圾。
注意:方法中的局部变量引用是否仍然有效,需要结合实际执行状态判断,并不是所有曾经出现过的局部变量都永远是 GC Roots。
2. 静态字段引用的对象
例如:
class Cache { static Object data = new Object();}data 是静态字段,引用着一个对象。当该静态字段仍然有效,并且能够从 GC Roots 到达这个对象时,对象就可以保持存活。
如果静态字段被清空,且对象没有其他可达路径,它就可能成为垃圾。
3. JNI 引用
JNI 是 Java Native Interface,即 Java 本地接口。
Java 可以通过 JNI 调用 C、C++ 等本地代码。本地代码可能持有 Java 对象的引用,这些有效引用也可能成为 GC Roots。
理解即可,普通 Java 后端开发中不需要深入 JNI 实现细节。
4. JVM 内部引用
JVM 内部某些重要的数据结构也可能持有对象引用,例如与类加载、运行时状态或同步机制有关的内部结构。
这类引用是否属于 GC Roots,取决于具体 JVM 实现和运行状态。面试初期知道存在这一类即可,不必死记内部细节。
用一个例子理解 GC Roots
class Demo { static Object global = new Object();
public void test() { Object local = new Object(); }}可以这样理解:
global:静态字段引用的对象,可能通过静态字段对应的根路径保持可达。local:方法执行期间的局部变量引用,其对象可能通过当前线程的栈帧保持可达。- 方法结束后,如果
local对应的对象没有其他可达路径,就可能成为垃圾。 - 只要
global仍然有效且引用对象,该对象就可能继续存活。
注意:示例用于说明引用关系。实际 GC Roots 的识别会考虑 JVM 运行状态,不应简单地把源代码中每一个局部变量或静态字段都视为永远有效的根。
常见误区
误区 1:被变量引用的对象就一定不会被回收
不完全正确。只有当引用本身有效,并且能够从 GC Roots 到达对象时,对象才能通过这条路径保持可达。
误区 2:静态变量引用的对象永远不会被回收
错误。静态字段可以被重新赋值或清空。如果对象失去所有 GC Roots 可达路径,就可能成为垃圾。
误区 3:两个对象互相引用就不会被回收
错误。
class Node { Node next;}如果对象 A 和对象 B 互相引用,但没有任何 GC Roots 能够到达它们,可达性分析仍然可以判定它们不可达。
面试回答模板
问:哪些对象可以作为 GC Roots?
答:
常见的 GC Roots 包括 Java 虚拟机栈中有效的引用、静态字段引用的对象、JNI 中持有的有效引用,以及 JVM 内部某些重要数据结构持有的引用。
JVM 以这些根对象为起点进行可达性分析,沿着引用关系遍历对象。如果某个对象无法从任何 GC Roots 到达,就会成为垃圾回收的候选对象。
需要记住的重点
- GC Roots 是可达性分析的起点。
- 常见类型:虚拟机栈中的引用、静态字段引用、JNI 引用、JVM 内部引用。
- 局部变量和静态字段并不意味着对象永远不会被回收。
- 对象之间互相引用,不代表它们一定存活。
- 是否存活的关键是:能否从 GC Roots 到达对象。
自测题
题目 1
方法执行结束后,局部变量引用的对象一定会立即被回收吗?
答案:不一定。如果对象仍然能通过其他有效引用从 GC Roots 到达,它就不会因为方法结束而变成不可达对象;即使已经不可达,也不意味着立即回收。
题目 2
静态字段引用的对象一定不会被回收吗?
答案:不一定。静态字段可以被清空或重新赋值;如果对象没有其他可达路径,就可能成为垃圾回收的候选对象。
题目 3
两个对象互相引用,但没有任何 GC Roots 能够到达它们,它们可以被回收吗?
答案:可以。可达性分析能够识别这种不可达的循环引用。