跳到主要内容
约 8 分钟4 张卡

GC Roots

GC Roots 是垃圾回收器遍历对象引用关系的起点。从这些根出发,沿引用能找到某个对象,就称这个对象可达。本文先用循环引用说明可达性分析,再介绍根的来源和四种引用,最后说明资源清理与类卸载的条件。

1. 面试核心​

先记住这几条,再读正文
  • 可达性。 HotSpot 从 GC Roots 遍历对象图,只有内部循环引用、没有外部可达路径的对象仍可回收。卡 · 引用计数与循环引用
  • 泄漏条件。 业务已经无用的对象仍被从 GC Roots 出发的强引用链持有时,就会形成内存泄漏。
  • 弱引用。 GC 发现对象弱可达时,会清除指向它的弱引用。卡 · 强、软、弱、虚引用
  • 弱键与强值。 可达的 WeakHashMap 强引用 value,value 再强引用 key 时,会阻止这个 key 被回收。卡 · 弱键与值的反向引用
  • 虚引用通知。 虚引用入队是程序可以处理的清理通知,无法据此断定目标对象的内存已经释放。

2. 引用计数与循环引用​

判断对象能否回收,要看程序能否从根找到它。单纯的引用计数只统计对象被引用的次数,遇到互相引用时就会留下问题。下面两个对象各自引用对方:

final class Node {
Node next;
}

Node left = new Node();
Node right = new Node();
left.next = right;
right.next = left;

left = null;
right = null;

把 left 和 right 设为 null 后,两个 Node 仍然互相引用。引用计数会看到每个对象还有一个引用,但从 GC Roots 出发已经找不到它们。因此,可达性分析可以把整个孤立的环一起回收。

3. GC Roots 的来源​

理解根的来源,可以帮助定位是谁让对象继续存活。常见的根与持有路径有以下四类:

  • 活动线程栈帧中的有效局部引用,以及维持线程执行所需的引用。
  • 可达类的静态字段,由类的对象图继续持有业务对象。
  • JNI 的局部引用和全局引用。JNI 是 Java 与本地代码交互的接口。
  • JVM 内部维持运行所需的引用,例如线程正在同步使用的对象引用。

线程栈扫描使用的是运行时有效引用信息。JIT 优化后,局部变量在源码中仍处于作用域内,也可以已经结束实际存活期。

静态字段属于类的对象图,而类也有自己的生命周期。类加载器及其对象图整体失去外部根后,内部的静态引用无法让它永久存活。根的显示名称和分类会随 JVM、分析工具变化,因此诊断时应追踪具体持有路径。

4. 可达对象与内存泄漏​

GC 能判断对象是否可达,但会话是否过期需要应用判断。下面的静态 Map 如果持续加入会话却从不删除,就会长期保留这些对象:

private static final Map<String, Session> SESSIONS = new HashMap<>();

只要该类仍可达,Map 中的 Session 就会沿静态字段这条路径被保留。会话即使已经过期,对 GC 来说仍可达,所以应用需要主动删除失效条目。

队列、监听器、ThreadLocal 和类加载器也能形成类似的持有链。Java 中这类泄漏的原因,是持有者的生命周期长于业务对象的有效期。修复时要找到谁负责解除引用,再处理删除、注销或任务结束时的清理。

5. 强、软、弱、虚引用​

引用强度决定对象仍被引用时,GC 可以怎样处理它。普通代码主要使用强引用,java.lang.ref 则提供软、弱、虚引用,用于不同的生命周期需求。

5.1 强引用​

强引用用于正常持有业务对象,普通字段和局部引用都属于这一类。从根出发始终沿强引用能到达的对象,称为强可达对象。即使内存紧张,GC 也会保留它。孤立对象内部的强引用环则仍可回收,因为根无法到达这个环。

5.2 软引用​

软引用允许 GC 在内存压力下清理对象,适合能够重新创建的辅助数据。对象只通过软引用等路径可达时,GC 会按内存压力和实现策略决定是否清除引用。在抛出 OOM 前,软可达对象的软引用会被清除。

这种清理时机由 JVM 决定,业务无法据此获得稳定的容量、过期时间或命中率。因此,业务缓存仍需要明确的容量和过期策略。

5.3 弱引用​

弱引用适合维护关联关系,同时避免这份关联独自延长对象的生命周期。当对象失去强可达和软可达路径、只剩弱引用等路径时,就达到弱可达状态。GC 发现这种状态后,会清除相应弱引用。

若引用注册了队列,引用对象会在清除时或稍后进入队列。清除时机取决于哪次收集发现了目标对象,所以应结合实际回收范围判断。

5.4 虚引用​

虚引用用于清理通知或生命周期跟踪。目标对象达到虚可达状态后,GC 会清除对应虚引用,引用对象在同时或稍后入队。程序可以根据通知处理独立保存的资源状态。

PhantomReference.get() 始终返回 null,因此程序无法通过它重新取得或复活目标对象。入队表示收到了清理通知,并不证明目标对象的内存已经释放。

5.5 弱键与值的反向引用​

WeakHashMap 的弱键适合这样的关联:键的其他所有者消失后,Map 中的条目也应允许清理。不过,Map 仍强引用 value。若 value 又强引用 key,就形成另一条可达路径:

可达的 WeakHashMap → value → key

此时 key 仍然强可达,弱键的清理条件就不成立。value 也可以经过其他 value 间接持有 key,所以只检查当前值的直接字段还不够。设计弱关联时,需要检查整个持有链。

6. ReferenceQueue​

引用被 GC 处理后,程序需要知道该执行哪项清理。ReferenceQueue 用于接收已注册引用的通知,队列里放的是引用对象本身。下面把一个虚引用注册到队列:

ReferenceQueue<ResourceOwner> queue = new ReferenceQueue<>();
PhantomReference<ResourceOwner> ref =
new PhantomReference<>(owner, queue);

应用需要保留 ref,并将它关联到对应的清理状态,才能识别后续通知。清理状态应独立保存,例如只保留待释放资源的句柄。若状态再次强引用 owner,目标对象就无法达到触发通知的可达性状态。消费队列的线程还需要明确停止条件,便于应用结束时退出。

6.1 Cleaner 与显式关闭​

文件和连接需要及时释放,优先通过 AutoCloseable 配合 try-with-resources 显式关闭。Cleaner 基于虚引用,可以作为遗漏关闭时的补充机制。它的执行时间取决于可达性判断和线程调度,所以适合补充清理,而非承担及时释放的责任。

清理任务 Runnable 应只保存释放资源所需的状态。若它捕获目标对象的 this,Cleaner 持有任务时也会继续持有目标对象。目标就无法触发自动清理。把清理状态放入静态嵌套类,可以避免隐式捕获外部对象。

Java 17 已弃用 finalization。Java 21 进一步将它标记为待移除。新资源管理代码应使用显式关闭与必要的 Cleaner。

7. 类卸载​

普通类能否卸载,要看定义它的类加载器是否具备回收条件。单个 Class 引用消失,尚不足以说明整组类可以卸载。热部署后 Metaspace 持续增长时,应检查旧加载器到 GC Roots 的路径。

具体卸载条件及线程、驱动注册等持有来源见类加载器。

8. 堆转储中的引用链​

堆转储能把“对象还在”变成可追踪的持有关系。分析具体实例时,按以下五步定位:

  1. 用 Histogram,也就是类直方图,找数量或对象自身大小异常的类型。
  2. 用 Dominator Tree,也就是支配树,找独自保留了大量内存的持有者。
  3. 查看 Path to GC Roots,追踪具体实例通向根的引用路径。
  4. 区分强引用路径与软、弱、虚引用路径,判断哪些关系会继续保留对象。
  5. 把持有者对应回缓存、队列、线程或类加载器,检查它们的生命周期。

对象可以同时被多条路径持有,所以最短路径只展示其中一条。修复后,还要比较相同负载下的 live set,也就是 GC 后存活占用,确认保留量已经稳定。

9. 常见问题​

9.1 两个对象循环引用会泄漏吗?​

孤立的引用环可以回收。如果这个环仍从 GC Roots 可达,GC 就会继续保留它。只有对象已经没有业务用途却仍被外部持有时,才构成泄漏。

9.2 把缓存值改成软引用就能避免 OOM 吗?​

9.3 finalize 适合用来释放资源吗?​

10 · 面试题卡片​

10.1 JVM 怎样判断一个对象可以回收​

出现公司:阿里巴巴、美团

考察重点:理解根遍历、孤立引用环与业务泄漏。

相关内容:引用计数与循环引用至可达对象与内存泄漏。

参考回答

HotSpot 通过可达性分析判断对象是否具备回收条件。

收集器从线程、JNI 和 JVM 内部引用等根出发,沿对象图继续遍历。根无法到达、且没有特殊引用处理要求的对象可以回收,因此孤立的循环引用也可整体回收。

业务过期但仍被强引用链持有的对象会继续存活。例如,会话仍留在静态缓存中,GC 就会保留它。应用需要在缓存、队列或监听器结束使用对象时解除引用。

10.2 强、软、弱、虚引用分别适合什么场景​

出现公司:阿里巴巴

考察重点:区分引用清除、队列通知与显式资源管理。

相关内容:强、软、弱、虚引用和ReferenceQueue。

参考回答

强引用用于正常持有对象,软引用用于可重新创建的数据,弱引用用于弱关联,虚引用用于清理通知。

这四种引用的处理方式分别是:

  • 强可达对象会继续保留,适合普通业务对象的生命周期管理。
  • 软引用按内存压力和实现策略清理,业务缓存仍需自行控制容量与过期。
  • 弱引用在 GC 发现目标弱可达后清除,适合避免关联结构独自延长目标生命周期。
  • 虚引用配合 ReferenceQueue 通知清理,get() 始终返回 null。入队也不表示目标内存已经释放。

文件和连接需要及时释放,仍应通过显式关闭或 try-with-resources 管理。

10.3 WeakHashMap 的 value 引用 key 时,key 还能自动回收吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释弱键之外的强引用路径,以及 value 反向持有 key 为什么阻止清理。

相关内容:弱键与值的反向引用。

参考回答

只要 Map 可达,且 value 强引用 key,这个 key 就无法按弱键机制回收。

因为 Map 强引用 value,value 又强引用 key,形成了绕过弱键的强引用路径。需要改变 value 的持有关系,才能让 key 达到弱可达状态。

10.4 Cleaner 的清理任务为何应避免捕获 this?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释清理任务持有目标对象时为什么无法触发自动清理,并区分清理状态与目标对象。

相关内容:Cleaner 与显式关闭。

参考回答

清理任务捕获 this 后,会继续强引用目标对象,阻止目标达到触发自动清理的状态。

Cleaner 保留任务时,也会沿这条引用保留目标。

可以将释放资源所需的状态放入独立的静态嵌套类,让任务只持有这些状态。资源有明确作用域时,仍应优先显式关闭。

11. 参考资料​