📌 PDF :大白话说Java面试题 --- 02-JVM篇
第29题:GC Roots 有哪些
📚 回答:
- 核心考点 :
GC Roots 是可达性分析算法 的起点。大厂面试要求准确列出所有类型的GC Roots ,并能解释为什么某些对象会成为GC Root 以及常见的误解(如静态变量是否一定能阻止GC?)。
1. GC Roots 的完整定义
GC Roots 是指必须存活的对象集合 ,从它们出发可达的对象被标记为存活 ,不可达的则判定为可回收。
2. GC Roots 的5种类型(JDK 8及之前)
| 类型 | 说明 | 示例 |
|---|---|---|
| 虚拟机栈引用 | 每个栈帧中的局部变量表引用的对象 | 方法内的局部变量、参数 |
| 静态属性引用 | 方法区中类的静态变量引用的对象 | private static User user = new User(); |
| 常量引用 | 方法区中运行时常量池引用的对象 | 字符串常量 "abc"、Class对象 |
| JNI引用(Native Stack) | 本地方法栈中JNI(全局/局部)引用的对象 | JNIEnv->NewObject(...) |
| 活跃线程 | 所有正在运行的Thread对象 | Thread.currentThread() |
3. 各类型的深度解释(面试加分项)
3.1 虚拟机栈中的引用(最常用)
-
包括:局部变量、方法参数、临时变量
-
示例:
javavoid foo() { Object o = new Object(); // o 是GC Root int i = 0; // 基本类型不是引用,不算 } -
注意:方法执行完出栈后,这些GC Root消失,对应对象变为不可达。
3.2 静态属性引用
-
属于类级别,类未被卸载则静态变量一直存活。
-
常见误解 :设置
static obj = null后,原对象不再被GC Root引用,可能被回收。 -
示例:
javaclass Cache { static Map<String, Object> map = new HashMap<>(); // map 是GC Root } -
内存泄漏高发地:静态集合类添加对象后忘记清理。
3.3 常量引用
- 字符串常量池中的对象(如
"hello") - Class对象(如
String.class) - 基本类型包装类常量(如
Integer.valueOf(1)缓存的 -128~127 对象) - 注意 :常量池中动态添加的字符串(
intern())也是GC Root,直到JVM回收该常量。
3.4 JNI 引用
- Global JNI Reference :显式
NewGlobalRef创建,需手动DeleteGlobalRef否则泄漏 - Local JNI Reference:本地方法栈帧内的引用,方法退出后自动释放
- 场景:Android NDK / JNI 开发中,忘记删除GlobalRef导致内存泄漏。
3.5 活跃线程
- 每个正在运行的线程本身就是GC Root(线程栈+程序计数器)
- 线程内部的局部变量也是从该线程Root可达的
4. 其他隐藏的 GC Roots(JDK 8+)
| 类型 | 说明 |
|---|---|
| 系统类加载器 | 加载核心类(rt.jar)的BootClassLoader |
| JVM内部对象 | SystemDictionary、JVMTI 标记的对象 |
| 同步监视器 | 被synchronized锁住的对象 |
| StackMapTable | JVM内部栈映射表引用的对象 |
| Finalizer引用 | 尚未执行finalize()的对象会被Finalizer队列持有 |
5. 关键区别:哪些不是 GC Roots?
| 对象 | 是否是GC Root | 原因 |
|---|---|---|
| 方法内的局部变量(未执行到) | 否 | 栈帧未入栈,不存在引用 |
| 不可达的静态变量 | 否 | 类已被卸载(如自定义ClassLoader卸载时) |
| 普通对象字段 | 否 | 需要通过GC Root链到达 |
| 软/弱/虚引用指向的对象 | 否 | 引用本身特殊处理,但引用的目标对象需链到Root才算存活 |
6. 大厂面试追问
Q1:静态变量引用的对象一定是 GC Root 吗?
A:是的,只要该类未被卸载。在自定义ClassLoader场景中,类可被卸载(如OSGi、热部署),卸载后静态变量不再作为GC Root。
Q2:字符串常量池中的对象永远不会被回收吗?
A:不会。JDK 7+字符串常量池在堆中,Full GC时若常量无引用,可以被回收。例如:String s = "a"; s = null; 触发Full GC后 "a" 可能被回收。
Q3:ThreadLocal 中的变量是 GC Root 吗?
A:ThreadLocalMap 的 key 是弱引用,不是直接GC Root ;但当前Thread对象是GC Root,从Thread可以访问到ThreadLocalMap,进而访问到key。所以只要线程活着,ThreadLocalMap的引用链就不断。
Q4:GC Roots 的数量一般是多少?
A:大应用中可达数万 甚至更多(每个栈帧的局部变量、每个静态变量、每个活跃线程等)。GC Roots 过多会导致可达性分析变慢。
Q5:可达性分析和引用计数法的区别?
A:引用计数无法解决循环引用(A->B, B->A),而可达性分析从GC Roots出发,不可达才回收,能正确处理循环。
7. 实战:如何查看一个对象是否被 GC Root 引用?
工具:MAT(Memory Analyzer Tool)
- 打开 heap dump
- 选择对象 → Path to GC Roots → 排除软/弱/虚引用
- 显示从该对象到某个GC Root的最短路径
示例结果:
java.lang.Object @ 0x7f3a2c8
→ com.example.Cache.map (静态字段)
→ java.util.HashMap @ 0x7f3a3d0
→ ... (到GC Root)
8. 总结对比表(面试速记)
| GC Root 类型 | 生命周期 | 常见泄漏风险 |
|---|---|---|
| 虚拟机栈引用 | 方法执行期间 | 无(自动释放) |
| 静态属性引用 | 类生命周期(通常永久) | 高(静态集合) |
| 常量引用 | JVM常量池回收时 | 中(intern字符串) |
| JNI Global引用 | 手动删除 | 高(忘记DeleteGlobalRef) |
| 活跃线程 | 线程运行期间 | 中(线程池任务未释放Context) |
💡 面试官想要的满分总结:
"GC Roots 是可达性分析的起点,主要包括5类:栈帧局部变量、静态变量、常量、JNI引用、活跃线程 。
特别注意:静态变量引用的对象只要类未卸载就一直是GC Root,容易引发内存泄漏;字符串常量池中的对象在JDK 7+中位于堆上,Full GC时可能被回收。
排查内存泄漏时,用MAT的 Path to GC Roots 功能,找到是哪种Root阻止了对象回收,就能定位问题根源。"
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯