gc roots是jvm判断对象存活的起点,指当前必须存活、不能被回收的引用入口;包括虚拟机栈局部变量、静态字段、常量池引用、jni全局引用、同步锁对象及jvm内部关键对象。

GC Roots 是 JVM 判断对象是否存活的起点,不是“重要对象”,而是“此刻必须活着、不能回收”的引用入口。只要一个对象能从任意 GC Root 出发、沿着引用链被触及,它就不会被回收;反之,哪怕对象之间互相引用,只要和所有 GC Root 断开,照样会被回收。
常见的 GC Roots 类型
这些不是固定类或实例,而是运行时动态确定的引用位置:
-
虚拟机栈中每个线程的栈帧局部变量表里的引用:比如方法里声明的
MyObject obj = new MyObject(),只要该方法没执行完、栈帧还在,obj 就是 GC Root。 -
方法区(元空间)中静态字段引用的对象:如
public static Map<string object> cache = new HashMap();</string>中的 map 实例本身是 GC Root,它引用的所有 key/value 都因此被“锁住”。 -
方法区常量池中已解析的常量引用的对象:包括字符串常量池里的字符串(JDK 7+ 在堆中,但仍是 GC Root)、
public static final String MSG = "ok";这类编译期常量所指向的对象。 -
本地方法栈中 JNI 全局引用(GlobalRef)指向的对象:C/C++ 代码通过
env->NewGlobalRef()持有的 Java 对象,会一直存活,直到显式调用DeleteGlobalRef()。 -
正在被 synchronized 锁住的对象:比如
synchronized(obj) { ... }执行期间,obj 本身成为 GC Root,防止锁对象在同步块内被回收。 -
JVM 内部关键对象:系统类加载器、基础类(如
String.class)、正在抛出的异常实例(如当前NullPointerException)、预分配的监控对象等。
GC Roots 直接导致内存泄漏的典型场景
问题不在于“有 GC Root”,而在于“不该长期持有的引用被意外固化为 GC Root”:
-
静态集合无清理:缓存用
static List<user> userList = new ArrayList();</user>,add 后忘了 remove 或 clear,User 实例永远无法脱离 GC Root 链路。 - ThreadLocal 持有强引用:ThreadLocal 的 value 存在 ThreadLocalMap 中,而该 Map 是线程栈的一部分;线程长时间存活(如线程池未关闭),value 就难被回收,尤其 value 又持有大对象时。
- JNI 全局引用未释放:本地代码创建 GlobalRef 后忘记 delete,对应 Java 对象永久驻留,且无法被任何 Java 层逻辑干预。
- 监听器/回调注册后未注销:GUI 或事件总线中,把对象注册为监听器,又没在销毁时反注册,该对象因被系统静态容器持有而变成 GC Root。
如何识别和规避这类问题
关键不是消灭 GC Root,而是控制它的生命周期与引用强度:
- 用
WeakHashMap替代HashMap做缓存,key 是弱引用,GC 时 key 被回收后 entry 自动失效。 - 静态集合加过期机制(如
ConcurrentHashMap+ 定时清理),或改用软引用包装 value(SoftReference<data></data>)。 - ThreadLocal 使用后显式调用
remove(),尤其在线程复用场景(如 Tomcat 线程池)下必须这么做。 - JNI 侧严格配对
NewGlobalRef / DeleteGlobalRef,避免裸指针长期持有。 - 借助工具分析:jstack 查线程栈、jmap -histo 查对象分布、MAT 分析支配树(Dominator Tree),重点看哪些对象被 static 字段或 JNI 引用牢牢锁住。











