GC 的核心问题是:如何判断一个对象是否“不再使用”?
JVM 采用了一种名为 可达性分析(Reachability Analysis) 的算法来解决这个问题。该算法的起点,就是我们本文要深入探讨的核心概念—— GC Roots。
可达性分析算法的基本思想是:
这个过程就像一个图的遍历,GC Roots 就是图的起始节点集合。任何无法从起始节点到达的节点,都将被清理。
成为 GC Roots 的对象必须具备一个关键特征:它们不通过堆内对象的引用,而是由 JVM 直接或间接持有,是程序运行中必须保持存活的“根基”。如果它们被回收,整个程序的运行将无法保证。
在 Java 中,常见的 GC Roots 主要包括以下几类:
这是最常见的一种 GC Root。在 Java 方法的执行过程中,所有局部变量(包括方法参数)都存储在线程的虚拟机栈(VM Stack)的栈帧(Stack Frame)中。只要方法尚未执行完毕退出,这些局部变量所引用的对象就是存活的。
public class GCRootsExample {
public void myMethod() {
// 'obj' 是一个局部变量,它存储在 myMethod 的栈帧中。
// 在 myMethod 执行期间,'obj' 就是一个 GC Root。
// 它所引用的 new Object() 对象是可达的。
Object obj = new Object();
System.out.println("Method is running...");
// 当 myMethod 执行完毕,栈帧出栈,'obj' 变量被销毁。
// 如果 new Object() 对象没有其他引用,它将变为不可达。
}
}
每个正在运行的 Java 线程本身就是一个 GC Root。因此,一个 java.lang.Thread
类的实例,只要它处于活动状态,它本身以及它所引用的其他对象都是可达的。
public class ThreadAsGCRoot {
public static void main(String[] args) throws InterruptedException {
// 'myThread' 是一个活动线程,因此它是一个 GC Root。
Thread myThread = new Thread(() -> {
// 只要该线程在运行,myThread 对象就一直存活。
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
myThread.start();
// 等待线程结束
myThread.join();
}
}
由 static
关键字修饰的变量属于类本身,其生命周期与类相同。当一个类被加载后,它的静态变量会一直存在于 JVM 的方法区(或元空间)中,直到该类被卸载。因此,静态变量引用的对象是典型的 GC Root。
public class StaticVariableGCRoot {
// 'STATIC_INSTANCE' 是一个静态变量,它是一个 GC Root。
// 它引用的 MyObject 实例在整个程序运行期间都可能是存活的,
// 除非 StaticVariableGCRoot 类被卸载。
private static final MyObject STATIC_INSTANCE = new MyObject("I am static");
public static void main(String[] args) {
// 即便没有其他引用指向 STATIC_INSTANCE,它也不会被回收。
System.gc(); // 建议 GC,但 STATIC_INSTANCE 仍然存活
System.out.println("GC finished.");
}
}
class MyObject {
private final String name;
public MyObject(String name) { this.name = name; }
}
注意:滥用静态变量,特别是静态集合(如
static List
或static Map
),是导致内存泄漏的常见原因。因为向这些集合中添加的对象会一直被 GC Root 引用,无法被回收。
通过 JNI 调用,Java 代码可以与本地代码(如 C/C++)进行交互。如果一个 Java 对象被传递到本地代码中,并且本地代码持有对该对象的引用(如全局引用),那么这个 Java 对象也会被视为 GC Root,以防止在本地代码使用它时被 GC 回收。
如果一个对象正在被用作 synchronized
锁,那么在同步块或同步方法执行期间,该对象是存活的,不能被垃圾回收。
public class SyncLockGCRoot {
private final Object lock = new Object();
public void performTask() {
new Thread(() -> {
// 在这个同步块执行期间,'lock' 对象就是一个 GC Root。
// 它确保了在多线程环境下,持有锁的对象不会被意外回收。
synchronized (lock) {
System.out.println("Thread is holding the lock.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
理解 GC Roots 对于排查内存泄漏至关重要。内存泄漏的本质是:某些对象逻辑上已经不再需要,但由于存在到 GC Roots 的引用链,导致 GC 无法回收它们。
常见的内存泄漏场景包括:
InputStream
、数据库连接等,它们底层可能通过 JNI 持有引用,如果不显式关闭,这些引用可能一直存在。