关于 JVM 中 GC Roots 的解释

25 阅读4分钟

《深入理解JVM》中对GC Roots的解释比较宽泛,导致不理解,这里简单解释一下

原文解释

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  3. 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  5. Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  6. 所有被同步锁(synchronized关键字)持有的对象。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

更容易理解的解释

1. 虚拟机栈(栈帧中的本地变量表)中的引用对象

  • 直白解释: 只要方法还没运行完,方法里定义的变量就不能删。

  • 场景: 比如你在 main 方法里写了 val user = User(),这里的user就是 GC Root

fun main() {
    val user = User()
    print(user.name)
}
  • 为什么是 Root: 此时 user 变量存在于当前线程的栈帧里。如果 GC 把这个 User 对象回收了,那你接下来的代码 user.name() 就会崩溃。

  • 总结: 正在运行的方法里用到的参数、局部变量都是 Root

方法区中类静态属性引用的对象

  • 直白解释:static 修饰的属性(注意是属性,而不是属性引用的那个对象),它们属于这个类,不属于某个对象。

  • 场景:

public class Test {
    public static Object cache = new Object();
}

这里抠一下字眼:按照书上的解释,cache就是被static修饰的属性,而cache指向的new Object就是GC Root,其实这是错误的❌。

真正的 GC Root,是这个 cache.

GC 的搜索逻辑是:

  1. 走到方法区,找到类。
  2. 看到类里有个静态变量 cache
  3. cache 标记为 Root。
  4. 顺着 cache 存的地址,去堆里找,找到了那个 Object
  5. 宣布这个 Object 是活的,不能被回收
  • 为什么是 Root: 静态变量的生命周期非常长,通常和类一致(直到类加载器卸载),它们往往被设计为全局共享,所以它们引用的对象必须是活的。

  • 总结: 全局变量(静态变量)是 Root

方法区中常量引用的对象

  • 直白解释: 主要是字符串常量池里的引用。

  • 场景: String s = "Hello World";

  • 为什么是 Root: 常量意味着 “不可变” 且 “长期存在”,为了性能,Java 会把字符串存在常量池里供全局复用,既然要复用,就不能随随便便被回收。

  • 总结: 常量池里的硬引用是Root

本地方法栈中 JNI 引用的对象

  • 直白解释: 当 Java 调用 C/C++ 代码(Native 方法)时,C/C++ 代码里可能也引用了 Java 对象。

  • 为什么是 Root: 虚拟机内部的 GC 管不了 C/C++ 的内存空间。如果 Java 这边把对象回收了,但 C/C++ 那边还在通过指针操作这个对象,就会导致系统崩溃。

  • 总结: 跨语言调用时,对方正在用的 Java 对象是 Root。

5. Java 虚拟机内部的引用

  • 直白解释: JVM 运行赖以生存的“基石”。

  • 场景: Object.class, Integer.class,或者是像 NullPointerException 这种经常被抛出的预分配异常对象。

  • 为什么是 Root: 如果连 Object 的类定义都被回收了,整个 Java 环境就瘫痪了。

  • 总结: 系统级必备对象Root

6. 所有被同步锁(synchronized)持有的对象

  • 直白解释: 正在被用来当“锁”的对象。

注意这里的「被用来当锁」,锁持有这个对象,其实是在用这个对象的锁🔒

  • 场景: synchronized(myLock) { ... } 只要有一个线程还没退出这个同步块,myLock 对象就不能动。

  • 为什么是 Root: 锁是多线程协作的关键。如果锁对象被回收了,线程之间的同步状态(谁持有锁、谁在等待)就会丢失,后果不堪设想。

  • 总结: 正在发挥“锁”作用的对象是 Root

7. JVM 内部情况的 JMXBean、JVMTI 注册的回调等

这一条不用记,可以忽略

  • 直白解释: 监控和调试工具留下的“钩子”。

  • 场景: 当你打开 VisualVM 观察内存,或者用 Java Agent 做性能监控时,JVM 需要维护一些回调函数和状态对象。

  • 为什么是 Root: 这些对象是为了让外部工具能“看透”虚拟机。如果它们被回收了,监控和调试功能就失效了。

  • 总结: 监控和调试系统关联的对象是 Root