gc roots 是 jvm 可达性分析中认定“一定存活”的引用入口,包括栈帧局部变量、jni 引用、static 字段、synchronized 对象和 jvm 内部对象;它们不依赖业务逻辑,而是由规范与运行时状态决定,错误持有会导致内存泄漏。

GC Roots 是什么:不是对象,是“活的起点”
GC Roots 不是某类特殊对象,而是 JVM 在做可达性分析时认定“一定存活、不能回收”的一组引用入口。只要从这些入口出发能触达的对象,就认为还在用;触达不到的,才可能被回收。
关键点在于:它们的存在不依赖于对象是否“有用”,而取决于 JVM 规范和运行时状态。比如一个正在执行的方法里的局部变量 obj,哪怕它后面再也不用了,只要栈帧还在,obj 就算 GC Root。
- 常见错误现象:
OutOfMemoryError: GC overhead limit exceeded有时就因为本该被回收的对象被 GC Root 错误持有了(比如静态集合不断 add) - 不是所有“全局变量”都是 GC Root:只有明确在规范里列出来的几类才算,比如
static字段、JNI 引用、Java 栈帧中的局部变量等 - JVM 实现差异会影响具体哪些东西算 Root:HotSpot 把“正在执行的本地方法栈里的 JNI 引用”也纳入,但某些嵌入式 JVM 可能不支持
哪些引用算 GC Root:按 JVM 规范列清楚
Java 虚拟机规范没强制规定具体实现,但 HotSpot 的实际行为非常稳定,以下五类最常遇到:
-
Java方法栈中每个栈帧的局部变量表里的引用(包括参数、临时变量) - 本地方法栈中 JNI 的
jobject(比如通过env->NewGlobalRef()创建的全局引用) - 所有
static字段(注意:字段本身是 Root,它引用的对象不是) - 正在被同步的
synchronized对象(即 monitor 持有者,如synchronized(obj)中的obj) - JVM 内部对象:如系统类加载器、重要的异常对象(
NullPointerException实例)、常量池中字符串等
容易踩的坑:ThreadLocal 的值本身不是 GC Root,但它持有的 ThreadLocalMap 是当前线程栈的一部分,所以线程没结束前,其中的 value 很难被回收——这也是内存泄漏高发区。
立即学习“Java免费学习笔记(深入)”;
为什么 static 字段容易引发内存泄漏
因为 static 字段属于类,而类对象由系统类加载器加载,系统类加载器本身是 GC Root,所以整个引用链锁死:GC Root → 类对象 → static 字段 → 实际对象。
- 典型场景:缓存用
public static Map<string object> cache = new HashMap();</string>,忘了清理过期项 - 参数差异:
WeakHashMap的 key 是弱引用,不会阻止 key 被回收;但static HashMap的 key 和 value 都强引用,全卡死 - 性能影响:这类对象长期驻留老年代,触发 Full GC 频率上升,且 CMS 或 G1 都无法绕过它做回收
实操建议:除非真需要进程级生命周期,否则别用 static 持有业务对象;改用 WeakReference 包一层,或交给 Spring 容器管理作用域。
如何验证某个对象是不是被 GC Root 持有
靠猜没用,得用工具看实际引用链。JDK 自带 jmap + jhat(已废弃)或更推荐 jcmd + jvisualvm / VisualVM 插件,或者直接用 jdk.jfr 录制 GC 事件。
- 快速命令:
jcmd <pid> VM.native_memory summary</pid>看内存分布,再用jmap -histo:live <pid></pid>看存活对象数量 - 关键操作:用
VisualVM打开 heap dump 后,右键对象 → “Merge shortest paths to GC Roots”,它会列出所有可到达路径 - 注意兼容性:JDK 17+ 默认禁用
java.lang.ref.Finalizer,所以旧版 dump 中常见的 “Finalizer” Root 在新版本里基本消失
复杂点在于:同一个对象可能被多个 Root 同时持有,而工具只显示“最短路径”,容易漏掉真正的问题源头。这时候得结合代码逻辑,逐条比对每条路径上的变量生命周期。










