GC Roots 有哪些
候选人小赵在面试蚂蚁 P6 时,面试官问道:
"GC Roots 包括哪些?线程栈上的局部变量可以作为 GC Roots 吗?"
小赵说:"可以..."面试官追问:"为什么线程的局部变量可以作为 GC Roots?线程在不断执行,局部变量也在变化..."
小赵彻底卡住了...
一、核心问题:GC Roots 有哪些 🔴
1.1 问题拆解
1.2 ❌ 错误示范
候选人原话 A:"GC Roots 就是 static 变量。"
问题诊断:static 变量只是 GC Roots 的一小部分。GC Roots 包括线程、栈、类、JNI 等多种类型。
候选人原话 B:"局部变量在栈上,栈是 GC Roots。"
问题诊断:GC Roots 不是栈本身,而是栈帧中的局部变量表里的引用类型对象。基本类型不是 GC Roots。
1.3 标准回答
P5 级别:GC Roots 的四大类型
GC Roots 的完整分类:
P6 级别:详解每种类型
1. 线程相关 GC Roots:
- 正在运行的线程:
Thread对象本身- ThreadLocalMap:
ThreadLocal持有的 Map(但 Map 中的 value 是 GC Roots)- synchronized 锁持有者:持有
synchronized锁的对象为什么局部变量可以作为 GC Roots?
局部变量存在于线程的虚拟机栈中,而栈帧本身不属于 GC Roots。GC Roots 是栈帧中的引用类型局部变量。因为:
- 当前线程正在执行的方法栈帧中的局部变量,一定是被当前线程直接引用的
- 如果一个对象被这些局部变量引用,它就不是垃圾(可达的)
- 当前线程永远可以访问自己的局部变量
2. 类相关 GC Roots:
- Class 对象:
Class对象本身(被 ClassLoader 持有)- static 字段引用:所有
static修饰的引用类型字段引用的对象3. JNI 引用:
JNI 代码可以持有 Java 对象的引用:
- Local JNI 引用:native 方法的参数、本地代码中通过 NewLocalRef 创建的引用
- Global JNI 引用:
NewGlobalRef创建的全局引用
P7 级别:GC Roots 在分析工具中的应用
OQL(Object Query Language)中的 GC Roots:
MAT 和 JProfiler 中可以看到对象到 GC Roots 的引用路径:
GC Roots 的可见性:
在 MAT 的 Heap Dump 分析中:
- Shallow Heap:对象自身占用的内存
- Retained Heap:对象 + 可达对象的总内存
- GC Root 本身是所有其他对象的起点,没有外部引用指向它
【面试官心理】 这道题我能问到 P7 级别,是因为 GC Roots 是内存分析的核心概念。能说清 GC Roots 在 MAT/JProfiler 中的实际应用的候选人说明他有内存调优的实战经验。
1.4 追问升级
追问:为什么基本类型不是 GC Roots?
因为 GC 管理的是对象,不是基本类型。基本类型(如
int、long)在栈帧中存储为值,不是引用。GC 通过引用链判断对象是否可达,基本类型不涉及引用。
二、生产避坑 🟡
2.1 大量 GC Roots 导致 Full GC
2.2 ThreadLocal 泄漏
ThreadLocal 的 value 通过 ThreadLocalMap 间接成为 GC Roots 可达的(通过 ThreadLocalMap → value),所以即使 ThreadLocal 本身不可达,ThreadLocalMap 中的 value 仍然存活。
面试加分点:能说出"JDK 11 的 Epsilon GC 不执行任何垃圾回收,可以用来测量纯对象分配开销,而不用担心 GC 干扰",说明他关注了 JDK 新特性。
面试陷阱:被问到"一个对象从 GC Root 不可达就一定会被回收吗",很多人会说"会"。准确答案是:不一定立即回收。对象从 GC Root 不可达后,只是成为了"可回收对象",真正被回收需要等待 GC 发生时清理。Object 的 finalize() 方法在对象第一次被 GC 标记后执行(如果被重写),第二次 GC 时对象才真正被回收。