GC Roots是HotSpot中一组必须活跃的引用,而非对象本身,包括:1.虚拟机栈中局部变量与参数;2.本地方法栈JNI全局引用;3.方法区static字段引用;4.字符串常量池引用;5.被synchronized持有的对象。

OpenJDK(以 HotSpot 为默认 VM)的 GC Roots 不是“对象”,而是**一组必须活跃的引用**,它们构成可达性分析的起点。只要从这些引用出发能触达某个堆对象,该对象就被视为存活;否则可能被回收。
哪些引用在 HotSpot 中实际算 GC Roots
HotSpot 的实现稳定且明确,以下五类引用在所有常见 GC 场景(包括 Young GC 和 Full GC)中均被纳入根集合:
-
Java 虚拟机栈中每个线程当前栈帧的局部变量表里的引用(含方法参数、临时变量),例如Object obj = new Object();中的obj -
本地方法栈中 JNI 创建的全局引用(env->NewGlobalRef())和部分强全局句柄;注意:弱全局引用(WeakGlobalRef)不算 -
方法区(元空间)中类的static字段所持有的引用,例如public static Map cache;中的cache引用本身(不是它指向的Map实例) -
方法区中字符串常量池(StringTable)对 interned 字符串实例的引用,如"abc"或显式调用String.intern()后的实例 - 正在被
synchronized持有的对象(即 monitor owner),哪怕只执行到synchronized(obj) { ... }的左大括号处,obj就已进入根集合
为什么 ThreadLocalMap 不是 GC Root,却常导致泄漏
ThreadLocal 的值本身不是 GC Root,但它的持有者——ThreadLocalMap 是当前线程栈帧的一部分(通过 Thread 对象的字段间接关联)。只要线程没结束,这个 Map 就不会被回收,而其中的 value 又被强引用着。更麻烦的是:ThreadLocalMap 的 Entry 使用弱引用指向 ThreadLocal 键,但对 value 是强引用——一旦忘记 remove(),value 就锁死在堆里。
- 典型泄漏场景:Web 容器中线程复用(如 Tomcat 的线程池),
ThreadLocal存了Connection或Context,但没清理 - 排查线索:
java.lang.Thread @ 0x...→threadLocals→ThreadLocalMap→table[i].value(用jmap -histo或 MAT 看支配树)
Minor GC 时的根集合比 Full GC 更“宽”
年轻代回收(Young GC)必须把老年代中指向年轻代的引用也加入根集合,否则跨代引用的对象会被误判为不可达。这类“额外根”包括:
- 老年代中所有
static字段、常量池引用、JNI 全局引用(只要它们指向年轻代对象) - Card Table 标记为 dirty 的老年代区域中,扫描出的所有跨代指针
- 这正是为什么 CMS(已废弃)和 G1 都要维护记忆集(
Remembered Set)——为避免每次Young GC扫全老年代
真正容易被忽略的点是:GC Roots 是动态的、上下文敏感的。同一个对象,在 synchronized 块内是 Root,块外就不是;一个 static 字段在类卸载前永远是 Root,但类卸载本身极难触发。排查泄漏时,别只盯着“哪些能当 Root”,更要问:“此刻它正被哪一类 Root 拖住?”


















