gc roots包括:虚拟机栈中局部变量引用的对象、本地方法栈jni全局引用的对象、方法区静态字段和运行时常量池中引用的对象、被synchronized锁定的对象;它们是可达性分析的起点,不直接扫描方法区(元空间),而是读取其中存储的引用值作为根。

Java垃圾回收器(GC)并不直接“扫描方法区”来查找根对象,因为从JDK 8开始,方法区的实现已由永久代(PermGen)替换为元空间(Metaspace),而元空间本身不参与常规GC的可达性分析。真正被GC扫描并用于根节点枚举的是共享堆内存中的活动对象引用,以及一些明确被认定为GC Roots的特殊位置——它们分布在堆外,但指向堆内对象。
哪些位置构成GC Roots?
GC Roots是可达性分析的起点,必须保证绝对可达、生命周期稳定。主要包括:
- 虚拟机栈(栈帧中的局部变量表):每个线程的栈中,正在执行的方法所持有的对象引用(如方法参数、局部变量)都是根
-
本地方法栈中JNI引用:Java调用C/C++代码时,通过JNI创建的全局或局部引用(如
jobject)若未被显式删除,会被视为根 -
方法区中部分静态属性:类的静态字段(
static修饰的引用类型变量)所指向的对象是根;注意:方法区本身(如Klass结构、常量池符号引用)不被扫描,但其中存储的静态变量值(即堆内对象地址)会被读取并作为根 - 运行时常量池中引用的对象:例如字符串常量池(String Table)里interned的字符串对象(JDK 7+后该池移至堆中),其引用目标在堆内,因此这些引用本身构成根
- 同步锁(Monitor)持有者:被synchronized锁定的对象,其引用在锁记录中保留,也会被视作根
堆内存如何被并发/安全扫描?
现代GC(如G1、ZGC、Shenandoah)采用多种机制保障堆扫描的正确性与低延迟:
- Stop-The-World(STW)阶段完成初始根枚举:在Minor GC或Mixed GC初始阶段,所有应用线程暂停,GC线程快速遍历各线程栈、JNI全局引用表、静态字段等,收集初始根集合
- 写屏障(Write Barrier)捕获并发修改:当应用线程在GC进行中修改引用(如给对象字段赋新值、更新数组元素),写屏障会记录该变更(如加入记忆集Remembered Set或更新标记位),确保新引用关系不被遗漏
- 并发标记阶段使用三色标记法:将对象标记为白(未访问)、灰(已发现但子引用未处理)、黑(已完全扫描)。配合SATB(Snapshot-At-The-Beginning)或增量更新(Incremental Update)策略,解决并发修改导致的漏标问题
方法区(元空间)为何不参与根扫描?
元空间位于本地内存,存放类元数据(Klass、Method、ConstantPool等),它不存储Java对象实例,也不保存指向堆对象的强引用(除静态字段和常量池引用外)。那些引用早已被提取并纳入前述GC Roots。元空间自身的内存管理由本地分配器负责,仅在类卸载时由GC触发清理——而类卸载的前提是:该类的ClassLoader不可达,且该类所有实例已被回收,这依赖于对堆和GC Roots的分析结果,而非反向扫描元空间。
一个典型例子:静态字段如何成为根
假设有类定义:public class Config { public static final List<string> PROPS = new ArrayList(); }</string>。虽然Config.class信息存于元空间,但PROPS这个静态字段的值是一个堆中ArrayList对象的地址。GC在枚举根时,会读取元空间中Config类的静态字段表,取出该地址,将其加入根集合——扫描动作发生在堆,元空间只是提供引用值的“源”,并非被扫描的“区域”。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











