GC Roots 有哪些

候选人小赵在面试蚂蚁 P6 时,面试官问道:

"GC Roots 包括哪些?线程栈上的局部变量可以作为 GC Roots 吗?"

小赵说:"可以..."面试官追问:"为什么线程的局部变量可以作为 GC Roots?线程在不断执行,局部变量也在变化..."

小赵彻底卡住了...

一、核心问题:GC Roots 有哪些 🔴

1.1 问题拆解

第一层:完整分类(有哪些?)
  "完整的 GC Roots 列表是什么?"
  考察点:线程/类/栈/JNI 四大类

第二层:为什么是根?(为什么选这些?)
  "为什么这些对象被选为 GC Roots?"
  考察点:可达性分析的正确性保证

第三层:实践应用(怎么用?)
  "GC Roots 在内存分析工具中怎么用?"
  考察点:MAT、JProfiler

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 的完整分类

类型具体内容
线程相关正在执行的线程对象、ThreadLocal 变量、synchronized 锁持有的对象
类相关Class 对象(类加载器加载的类)、static 字段引用的对象
栈相关方法局部变量表中的引用类型对象、方法参数
JNI 引用JNI 全局引用、Local/Global JNI 引用

P6 级别:详解每种类型

1. 线程相关 GC Roots

  • 正在运行的线程Thread 对象本身
  • ThreadLocalMapThreadLocal 持有的 Map(但 Map 中的 value 是 GC Roots)
  • synchronized 锁持有者:持有 synchronized 锁的对象

为什么局部变量可以作为 GC Roots?

局部变量存在于线程的虚拟机栈中,而栈帧本身不属于 GC Roots。GC Roots 是栈帧中的引用类型局部变量。因为:

  • 当前线程正在执行的方法栈帧中的局部变量,一定是被当前线程直接引用的
  • 如果一个对象被这些局部变量引用,它就不是垃圾(可达的)
  • 当前线程永远可以访问自己的局部变量

2. 类相关 GC Roots

  • Class 对象Class 对象本身(被 ClassLoader 持有)
  • static 字段引用:所有 static 修饰的引用类型字段引用的对象
public class MyClass {
    static Object obj = new Object();  // obj 指向的对象是 GC Root
    static String str = "hello";       // 常量也是 GC Root
}

3. JNI 引用

JNI 代码可以持有 Java 对象的引用:

  • Local JNI 引用:native 方法的参数、本地代码中通过 NewLocalRef 创建的引用
  • Global JNI 引用NewGlobalRef 创建的全局引用

P7 级别:GC Roots 在分析工具中的应用

OQL(Object Query Language)中的 GC Roots

MAT 和 JProfiler 中可以看到对象到 GC Roots 的引用路径:

-- MAT 中的 OQL 查询:查找到 GC Root 的路径
SELECT path from objects where className in ("com.example.MyClass")

GC Roots 的可见性

在 MAT 的 Heap Dump 分析中:

  • Shallow Heap:对象自身占用的内存
  • Retained Heap:对象 + 可达对象的总内存
  • GC Root 本身是所有其他对象的起点,没有外部引用指向它

【面试官心理】 这道题我能问到 P7 级别,是因为 GC Roots 是内存分析的核心概念。能说清 GC Roots 在 MAT/JProfiler 中的实际应用的候选人说明他有内存调优的实战经验。

1.4 追问升级

追问:为什么基本类型不是 GC Roots?

因为 GC 管理的是对象,不是基本类型。基本类型(如 intlong)在栈帧中存储为值,不是引用。GC 通过引用链判断对象是否可达,基本类型不涉及引用。

二、生产避坑 🟡

2.1 大量 GC Roots 导致 Full GC

// 危险:大量持有引用的静态变量
static List<byte[]> cache = new ArrayList<>();

// 每次添加:cache 持有的 byte[] 无法被回收
// cache 本身是 GC Root

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 时对象才真正被回收。