1. GC Roots是什么:先说清楚垃圾回收的判定源头
做Java开发的朋友,迟早会撞上“GC Roots”这个词。尤其是在排查内存泄漏、调优Full GC延迟的时候,GC Roots几乎是绕不开的入口。我最初看《深入理解Java虚拟机》时,对GC Roots的理解只停留在“从这些根开始遍历,能到达的对象就是活的”这个层面,直到自己实际分析堆转储、定位线上OOM,才真正意识到GC Roots在垃圾回收里的分量。
简单说,JVM判断一个对象能不能回收,目前主流的商用虚拟机(比如HotSpot)用的都是可达性分析算法。这个算法的核心逻辑很朴素,就是从一组称为“GC Roots”的根节点出发,沿着引用链向下搜索,走过的路径叫Reference Chain。如果一个对象到任何一个GC Root都没有任何引用链相连,那这个对象就会被判定为“不可达”,也就是可以被回收的候选对象。
那为什么不直接用引用计数?引用计数实现简单,但解决不了循环引用的问题——A引用了B,B也引用了A,但它们俩谁都不被外部持有,计数器永远不为零,垃圾就一直赖在堆里。可达性分析天然规避了这个问题,它只关心从根出发能不能到,不关心互相指向。
GC Roots的选择是整个可达性分析的前提。根选得不对,要么漏掉本该存活的对象导致程序崩溃,要么把本该回收的对象错误保留导致内存泄漏。所以搞清楚“什么能当GC Root”,比背下可达性的定义更能帮助你理解JVM的回收行为。这篇文章我就把GC Roots的底层逻辑、具体类型和实际排查方法一次讲透,适合正在学习JVM、准备面试,或者已经在线上环境被GC问题折磨的工程师。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 可达性分析与GC Roots:JVM如何从根上判断生死
2.1 从根出发的存活判定
要理解GC Roots,先得理解JVM为什么选择“根”这个概念。想象一个学校要统计哪些学生还有人联系——如果只靠学生之间互相联系来判断,就可能出现一个班级内部互相发消息但整个班已经被遗弃的情况。正确的做法是,从校长、班主任这类“源头人物”出发,沿着通讯录逐级找到所有能联系到的学生,只有从源头无法触达的人,才算是真正失联。
JVM里的“源头人物”就是GC Roots。可达性分析从这些根开始,顺着对象间的引用关系一层层遍历,凡是遍历到的对象都标记为“活着”。整个过程不需要对象自己报告状态,而是由虚拟机主动扫描,所以不受循环引用的干扰。
这里有个细节很多人会忽略:可达性分析必须在一个一致性的快照中进行,比如在Stop The World(STW)状态下。因为如果一边遍历一边有线程在修改引用关系,结果就没法保证。这也解释了为什么GC时应用线程要暂停——不是JVM故意拖慢你,而是它需要保证分析结果可靠。
2.2 GC Roots在堆外而非堆内
一个常见误区是,GC Roots是不是堆里的某些特殊对象?不是。GC Roots本身通常是栈上的引用、静态变量、JNI引用等,它们在内存里属于堆外区域(比如虚拟机栈、方法区)。堆内的对象只是被这些外部引用的链条拉住的。
理解这个区别很重要。做堆转储分析时,你用MAT看到的“GC Roots”列表,本质上是JVM帮你从堆外找出来、能直接或间接引用堆内对象的那些入口。也就是说,GC Roots是“外部对堆的引用入口”,不是堆内的对象。
2.3 为什么说“枚举根节点”是GC的开销大户
GC发生时,第一步就是从各个线程栈、全局变量、JNI句柄里找出所有GC Roots。这一步叫根枚举。它不像后面的标记过程那样可以并发,需要暂停用户线程,而且根的数量直接影响暂停时间。
特别在线程数很多的服务里,每个线程都有独立的虚拟机栈,栈里又有很多局部变量可能引用堆对象。JVM必须把这些栈帧里的引用全部当作GC Roots来扫描。所以你会发现,线程越多,GC Roots越多,扫描越耗时。这也是为什么很多高并发服务会把线程池大小控制在合
