← 返回题目列表

JVM 如何判断一个对象可以被回收?

高频 中等 第 7 / 34 题 更新于 2026/07/25
垃圾回收可达性分析GC Roots

简化版

主流 JVM 用可达性分析:从一组叫 GC Roots 的根对象出发,沿引用链往下搜,搜不到(不可达)的对象就判定为可回收。它不用引用计数,因为引用计数解决不了循环引用。

详细版

判断对象存活有两种思路:

  • 引用计数:给对象加个计数器,被引用 +1、引用失效 -1,为 0 就回收。简单高效,但无法处理循环引用(A 引用 B、B 引用 A,两者计数都不为 0,即使外部都不用了也回收不掉),所以 JVM 不采用。
  • 可达性分析(JVM 采用):以 GC Roots 为起点向下搜索,走过的路径叫「引用链」。一个对象到 GC Roots 没有任何引用链相连(不可达),就是可回收对象。

能作为 GC Roots 的典型有:虚拟机栈里局部变量引用的对象、方法区里静态字段和常量引用的对象、本地方法栈 JNI 引用的对象、被 synchronized 持有的锁对象等——共同点是「一定还活着的引用起点」。

注意:不可达表示对象已不再被强可达链保护,但软引用、弱引用、终结处理等会影响具体处理时机。finalize() 已被弃用并计划移除,业务代码不应把它当作“自救”或资源释放机制。

完整版教学

一、为什么不用引用计数

引用计数直观:对象被引用就计数加一。它的致命伤是循环引用

class Node { Node next; }
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null; // a、b 互相引用,计数都=1,永远回收不了 → 内存泄漏

外部已经不用 a、b 了,但它们互相引用,计数永远不为 0。可达性分析没这个问题:从 GC Roots 出发根本走不到这两个对象,直接判死。

二、可达性分析怎么走

把对象想成一张有向图,GC Roots 是「树根」。GC 时从所有根出发做一次搜索(可理解为遍历),凡是能走到的都是存活对象;走不到的,就是垃圾。

关键在于根的选择——GC Roots 必须是「此刻绝对还在用」的引用来源:

  • 虚拟机栈中各线程局部变量表引用的对象(正在执行的方法里用到的)。
  • 方法区中类静态属性、常量引用的对象。
  • 本地方法栈中 JNI(native 方法)引用的对象。
  • synchronized 锁持有的对象、JVM 内部引用(系统类加载器、基本异常对象)等。

三、扫描 GC Roots 时怎样保证一致性

应用线程一直在修改对象引用。如果收集器一边遍历对象图,业务线程一边把引用从 A 改到 B,就可能漏标仍在使用的对象。因此,根扫描和并发标记都需要一致性机制。

HotSpot 会在安全点读取栈和寄存器状态,并借助 OopMap 等元数据识别哪些位置保存对象引用,而不是把每个机器字都猜成指针。不同收集器还会使用写屏障与记忆集,记录并发期间发生的引用变化。

GC Roots → A → B
业务线程同时执行:A.child = C
收集器必须保证 B 或 C 不会因并发修改而被错误漏标

“从 GC Roots 出发遍历”描述的是算法模型;安全点、OopMap、写屏障等才是 HotSpot 落地时保证正确性和效率的关键机制。

四、哪些引用通常会成为 GC Roots

根来源典型例子生命周期边界
Java 线程执行状态栈帧局部变量、寄存器中的引用随栈帧和线程变化
类静态字段已加载类的静态引用字段受类及类加载器可达性影响
JNI 引用JNI 全局引用、活动本地调用中的引用需由本地代码按规则释放
JVM 内部结构系统类加载器、运行时维护对象由具体 JVM 实现决定
同步监视器被活动监视器持有的对象随锁状态变化

“字符串常量池里的对象永远是根”并不严谨。根的具体枚举方式属于 JVM 实现细节,对象是否存活最终仍要看当次可达关系。

五、可回收不等于立刻释放

可达性分析得到的是某次收集中的存活判断,不承诺垃圾对象会在某个确定时刻释放。只有发生相应范围的 GC,并完成引用处理、标记、转移或清扫后,空间才可能重新可用。

软引用和弱引用还会进入专门的引用处理流程:

  • 强可达对象不会因内存压力被回收;
  • 软引用对象的清理策略由实现和内存压力决定;
  • 弱引用对象只剩弱可达时,会在相应 GC 处理中被清理;
  • 虚引用用于配合 ReferenceQueue 接收回收相关通知,get() 始终返回 null

finalize() 自 Java 9 起被弃用,JDK 18 又将终结机制标记为待移除。即使某个 JDK 仍支持它,也不保证及时执行,且终结器可能拖慢回收或让对象短暂复活。资源应使用 try-with-resources、显式 close();必要的兜底清理可考虑 Cleaner,但它同样不能替代及时关闭。

六、用一个对象图做定量判断

假设堆中有 6 个对象,引用关系如下:

Root1 → A → B
Root2 → C
D ↔ E
F 无任何入边

从两个根出发只能访问 A、B、C,因此它们存活;D 和 E 虽然互相引用,但没有来自根的路径,仍属于不可达;F 也不可达。

用集合表示就是:

可回收候选 = 堆中对象集合 - 从 GC Roots 可达的对象集合
             = {A,B,C,D,E,F} - {A,B,C}
             = {D,E,F}

这正是可达性分析能解决循环引用,而单纯引用计数难以解决的原因。

七、常见误区与追问

  • 误区:互相引用的两个对象一定不会被 JVM 回收。 只要从任何 GC Root 都到不了它们,循环引用仍可被可达性分析识别为垃圾。
  • 误区:对象不可达后内存会立即归还。 不可达只是回收资格,真正释放取决于后续 GC 范围和执行进度。
  • 误区:finalize() 是可靠的对象自救与资源关闭方案。 它已被弃用且执行不及时,业务资源必须显式关闭。
  • 追问:为什么扫描线程栈不能把所有数值都当成引用? 精确式 GC 借助 OopMap 等元数据定位引用,避免保守猜测造成误保留和移动困难。
  • 追问:类静态字段引用的对象一定永久存活吗? 不一定;当类加载器及相关类不再可达并满足卸载条件时,这条根链可能消失。
  • 追问:JNI 为什么容易造成泄漏? 本地代码若长期保留 Global Reference 而不删除,对象会持续从 JNI 根可达。
  • 追问:可达性分析为什么需要 STW? 至少根快照等阶段通常需要一致状态;并发收集器再用屏障等机制处理并发修改,暂停范围因收集器而异。

回答这道题时先讲“根到对象的路径”,再列典型根,最后补上并发一致性与引用处理,逻辑会比背一串名词完整得多。

八、加强记忆

记住“根、链、不可达”三步:先枚举线程执行状态、静态字段、JNI、监视器和 JVM 内部结构等根,再沿引用链遍历,最后把走不到的对象列为回收候选。循环引用不可怕,因为没有根路径的环仍然不可达;真正困难的是并发修改,因此 HotSpot 还需要安全点、OopMap 和写屏障保证扫描一致性。软、弱、虚引用会进入各自引用处理流程,所以不可达不等于内存立即释放。资源管理不依赖已弃用的 finalize(),而要靠明确的 close() 生命周期。