深入 JVM 垃圾回收:如何判断对象可以被垃圾回收?
作者 :Weisian
发布时间:2026 年 2 月 25 日

📌 系列导读 :在前几篇中,我们依次建立了 JVM 的全局认知、详解了运行时数据区、深入分析了堆内存结构、探讨了类加载机制。今天,我们来探讨垃圾回收的前置核心问题 ------如何判断对象可以被垃圾回收。
这道题在 Java 中高级面试中的出现率超过 80% ,是理解垃圾回收机制的理论基础。不理解对象死亡判定,就无法理解 GC 算法、收集器工作原理、内存泄漏排查等后续内容。
如果说类加载机制是类的"出生证明",那么垃圾回收就是对象的"死亡判定"。理解 JVM 如何判断对象可以回收,是区分普通程序员和资深工程师的关键。
今天,我们将从引用计数法、可达性分析、GC Roots 范围、对象回收过程、四种引用类型 五个维度,层层递进地拆解这道面试必考题,并附上创作思路、得分要点、避坑指南,助你面试中脱颖而出。
一、对象死亡判定方法 ------ 两种算法对比
1.1 引用计数法(Reference Counting)
引用计数法 是最直观的对象死亡判定方法,但Java 并未采用。
┌─────────────────────────────────────────────────────────────────┐
│ 引用计数法原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 核心思想: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1. 每个对象维护一个引用计数器 │ │
│ │ 2. 对象被引用时,计数器 +1 │ │
│ │ 3. 引用失效时,计数器 -1 │ │
│ │ 4. 计数器为 0 时,对象可被回收 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 示例: │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 对象 A │ │ 对象 B │ │ 对象 C │ │
│ │ count=2 │ │ count=1 │ │ count=0 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ ↑ ↑ ↑ │ │
│ │ └───────────────┘ │ │
│ └─────────────────────────────────────┘ │
│ 引用关系 │
│ │
│ 结果:对象 C 可回收(count=0),对象 A、B 不可回收 │
│ │
└─────────────────────────────────────────────────────────────────┘
优点与缺点
| 维度 | 说明 |
|---|---|
| 优点 | 实现简单、判定效率高、无需暂停线程 |
| 缺点 | 无法解决循环引用问题(致命缺陷) |

循环引用问题示例
java
public class ReferenceCountingDemo {
private Object instance;
public static void main(String[] args) {
// 创建两个对象
ReferenceCountingDemo objA = new ReferenceCountingDemo();
ReferenceCountingDemo objB = new ReferenceCountingDemo();
// 互相引用(循环引用)
objA.instance = objB; // objA 引用 objB
objB.instance = objA; // objB 引用 objA
// 断开外部引用
objA = null;
objB = null;
// 问题:objA 和 objB 的引用计数都不为 0
// 但实际已无法访问,应该被回收
// 引用计数法无法解决这个问题!❌
}
}
┌─────────────────────────────────────────────────────────────────┐
│ 循环引用问题示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 外部引用断开后: │
│ │
│ ┌───────────┐ ┌───────────┐ │
│ │ objA │ ←─────→ │ objB │ │
│ │ count=1 │ │ count=1 │ │
│ └───────────┘ └───────────┘ │
│ ↑ ↑ │
│ │ │ │
│ └─────────────────────┘ │
│ 互相引用 │
│ │
│ 问题: │
│ - 外部引用已断开(objA = null, objB = null) │
│ - 但 count 都不为 0(因为互相引用) │
│ - 引用计数法认为不可回收,但实际已无法访问 │
│ - 导致内存泄漏!⚠️ │
│ │
└─────────────────────────────────────────────────────────────────┘
✅ 面试金句 :
"引用计数法实现简单,但无法解决循环引用问题。Java 采用可达性分析算法,从根本上避免了这个问题。"
1.2 可达性分析算法(Reachability Analysis)
可达性分析算法 是 Java 采用的对象死亡判定方法,通过GC Roots作为起点,向下搜索引用链。
┌─────────────────────────────────────────────────────────────────┐
│ 可达性分析算法原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 核心思想: │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1. 从 GC Roots 开始向下搜索 │ │
│ │ 2. 搜索过的路径称为引用链 │ │
│ │ 3. 无法从 GC Roots 到达的对象判定为可回收 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
│ 示例: │
│ │
│ ┌─────────────┐ │
│ │ GC Roots │ │
│ └──────┬──────┘ │
│ │ │
│ ┌────┴────┐ │
│ ▼ ▼ │
│ ┌───┐ ┌───┐ │
│ │ A │ │ B │ ← 可达对象(不可回收) │
│ └─┬─┘ └─┬─┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌───┐ ┌───┐ │
│ │ C │ │ D │ ← 可达对象(不可回收) │
│ └───┘ └─┬─┘ │
│ │ │
│ ▼ │
│ ┌───┐ │
│ │ E │ ← 可达对象(不可回收) │
│ └───┘ │
│ │
│ ┌───┐ ┌───┐ │
│ │ F │ ←─→ │ G │ ← 不可达对象(可回收)⚠️ │
│ └───┘ └───┘ │
│ ↑ ↑ │
│ └─────────┘ │
│ 循环引用 │
│ │
│ 结果:A、B、C、D、E 可达(不可回收),F、G 不可达(可回收)✅ │
│ │
└─────────────────────────────────────────────────────────────────┘
优点与缺点
| 维度 | 说明 |
|---|---|
| 优点 | 解决循环引用问题、判定准确、Java 官方采用 |
| 缺点 | 需要暂停线程(STW)、实现复杂 |

与引用计数法对比
| 对比项 | 引用计数法 | 可达性分析 |
|---|---|---|
| Java 是否采用 | ❌ 否 | ✅ 是 |
| 循环引用 | 无法解决 | 完美解决 |
| 实现复杂度 | 简单 | 复杂 |
| 执行效率 | 高(无需 STW) | 中(需要 STW) |
| 准确性 | 低 | 高 |
✅ 面试金句 :
"可达性分析通过 GC Roots 作为起点,能准确判断对象的可达性,从根本上解决了循环引用问题。这是 Java 选择它的核心原因。"
二、GC Roots 详解 ------ 哪些对象可以作为根节点
2.1 GC Roots 的范围
GC Roots是可达性分析的起点,只有从 GC Roots 可达的对象才会被保留。
┌─────────────────────────────────────────────────────────────────┐
│ GC Roots 范围 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1. 虚拟机栈中引用的对象(栈帧中的局部变量) │ │
│ │ - 方法参数 │ │
│ │ - 局部变量 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 2. 方法区中静态属性引用的对象 │ │
│ │ - static 修饰的字段 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 3. 方法区中常量引用的对象 │ │
│ │ - final static 常量 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 4. 本地方法栈 JNI 引用的对象 │ │
│ │ - Native 方法持有的 Java 对象引用 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 5. JVM 内部引用 │ │
│ │ - 类加载器 │ │
│ │ - 异常对象 │ │
│ │ - 系统类加载器 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↑ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 6. 被同步锁持有的对象 │ │
│ │ - synchronized 锁对象 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

2.2 GC Roots 详解表
| GC Roots 类型 | 说明 | 示例 |
|---|---|---|
| 虚拟机栈引用 | 栈帧中局部变量表引用的对象 | 方法参数、局部变量 |
| 静态属性引用 | 方法区中 static 字段引用的对象 | static Object obj |
| 常量引用 | 方法区中 final static 常量引用的对象 | final static String CONST |
| JNI 引用 | 本地方法栈中 Native 方法持有的引用 | nativeMethod() 中的 Java 对象 |
| JVM 内部引用 | JVM 自身使用的对象引用 | 类加载器、异常对象 |
| 同步锁持有 | 被 synchronized 锁持有的对象 | synchronized(obj) 中的 obj |
2.3 代码示例:各种 GC Roots
java
public class GCRootsDemo {
// 2. 静态属性引用(GC Roots)
private static Object staticObj = new Object();
// 3. 常量引用(GC Roots)
private static final String CONSTANT = "hello";
// 实例变量(不是 GC Roots,需要被 GC Roots 引用才可达)
private Object instanceObj = new Object();
public void method1(Object param) {
// 1. 虚拟机栈引用(GC Roots)
Object localVar = new Object(); // 局部变量
useObject(param); // 方法参数
useObject(localVar);
}
public synchronized void method2() {
// 6. 被同步锁持有的对象(GC Roots)
// this 对象被 synchronized 持有
Object lockObj = new Object();
synchronized (lockObj) {
// lockObj 在同步块内是 GC Roots
useObject(lockObj);
}
}
public native void nativeMethod(); // 4. JNI 引用(GC Roots)
private void useObject(Object obj) {
// 使用对象
}
public static void main(String[] args) {
GCRootsDemo demo = new GCRootsDemo();
demo.method1(new Object());
demo.method2();
}
}
2.4 可达性分析示意图
┌─────────────────────────────────────────────────────────────────┐
│ 对象可达性分析示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ GC Roots: │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 栈帧局部变量│ │ 静态字段 │ │ 常量池 │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───┐ ┌───┐ ┌───┐ │
│ │ A │ │ B │ │ C │ ← 可达对象 │
│ └─┬─┘ └─┬─┘ └─┬─┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───┐ ┌───┐ ┌───┐ │
│ │ D │ │ E │ │ F │ ← 可达对象 │
│ └───┘ └───┘ └───┘ │
│ │
│ ┌───┐ ┌───┐ │
│ │ G │ ←─→ │ H │ ← 不可达对象(可回收)⚠️ │
│ └───┘ └───┘ │
│ ↑ ↑ │
│ └─────────┘ │
│ 循环引用 │
│ │
│ ┌───┐ │
│ │ I │ ← 不可达对象(可回收)⚠️ │
│ └───┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
✅ 面试金句 :
"GC Roots 是可达性分析的起点,只有从 GC Roots 可达的对象才会被保留。理解 GC Roots 的范围,是分析内存泄漏的关键。"
三、对象回收过程 ------ 从标记到清理
3.1 对象回收的三个阶段
┌─────────────────────────────────────────────────────────────────┐
│ 对象回收过程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 阶段 1:第一次标记 │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ - 从 GC Roots 开始遍历,标记所有可达对象 │ │
│ │ - 未被标记的对象进入"待回收"状态 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段 2:执行 finalize() 方法(可选) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ - 如果对象重写了 finalize() 方法,会被放入 F-Queue │ │
│ │ - 由 Finalizer 线程执行 finalize() 方法 │ │
│ │ - 对象可在 finalize() 中自救(重新建立引用) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 阶段 3:第二次标记 │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ - 对 F-Queue 中的对象再次标记 │ │
│ │ - 自救成功的对象移除"待回收"状态 │ │
│ │ - 自救失败或未自救的对象正式回收 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

3.2 finalize() 方法详解
java
public class FinalizeDemo {
private static FinalizeDemo resurrectedObj = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
// 对象自救:在 finalize() 中重新建立引用
resurrectedObj = this;
System.out.println("对象自救成功!");
}
public static void main(String[] args) throws InterruptedException {
FinalizeDemo obj = new FinalizeDemo();
obj = null; // 断开引用
// 触发 GC
System.gc();
Thread.sleep(1000); // 等待 Finalizer 线程执行
// 检查对象是否自救成功
if (resurrectedObj != null) {
System.out.println("对象还活着!");
} else {
System.out.println("对象已死亡!");
}
// ⚠️ 注意:finalize() 只能自救一次
resurrectedObj = null;
System.gc();
Thread.sleep(1000);
if (resurrectedObj != null) {
System.out.println("对象还活着!");
} else {
System.out.println("对象已死亡!"); // 这次会输出
}
}
}
finalize() 的特点
| 特点 | 说明 |
|---|---|
| 只能调用一次 | 对象的 finalize() 方法只会被执行一次 |
| 可以自救 | 在 finalize() 中重新建立引用,对象可复活 |
| 不推荐使用 | JDK 9 已废弃,性能差、行为不确定 |
| 替代方案 | 使用 java.lang.ref.Cleaner |
3.3 对象状态流转图
┌─────────────────────────────────────────────────────────────────┐
│ 对象状态流转图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ │
│ │ 可达状态 │ ← GC Roots 可达 │
│ └─────┬─────┘ │
│ │ 断开引用 │
│ ▼ │
│ ┌───────────┐ │
│ │ 第一次标记 │ ← 未被标记,进入"待回收"状态 │
│ └─────┬─────┘ │
│ │ │
│ ├─── 无 finalize() ───→ ┌───────────┐ │
│ │ │ 正式回收 │ │
│ │ └───────────┘ │
│ │ │
│ └─── 有 finalize() ───→ ┌───────────┐ │
│ │ F-Queue │ │
│ └─────┬─────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ 执行 finalize()│ │
│ └─────┬─────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 自救成功 │ │ 自救失败 │ │ 无自救 │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 移除标记 │ │ 正式回收 │ │ 正式回收 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
四、四种引用类型 ------ 影响对象回收的关键
4.1 四种引用类型对比
┌─────────────────────────────────────────────────────────────────┐
│ 四种引用类型对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 引用强度:强引用 > 软引用 > 弱引用 > 虚引用 │
│ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 1. 强引用(Strong Reference) │ │
│ │ - 代码中直接赋值的引用 │ │
│ │ - 只要引用存在,对象永远不会被回收 │ │
│ │ - 示例:Object obj = new Object(); │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 2. 软引用(Soft Reference) │ │
│ │ - 内存不足时会被回收 │ │
│ │ - 适合做缓存 │ │
│ │ - 示例:SoftReference<Object> softRef = ... │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 3. 弱引用(Weak Reference) │ │
│ │ - 下次 GC 时会被回收 │ │
│ │ - 适合临时对象缓存 │ │
│ │ - 示例:WeakReference<Object> weakRef = ... │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ 4. 虚引用(Phantom Reference) │ │
│ │ - 不影响对象生命周期 │ │
│ │ - 只能配合 ReferenceQueue 使用 │ │
│ │ - 示例:PhantomReference<Object> phantomRef = ... │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 四种引用类型详解表
| 引用类型 | 回收时机 | 典型场景 | 代码示例 |
|---|---|---|---|
| 强引用 | 永不回收(除非断开引用) | 普通对象引用 | Object obj = new Object() |
| 软引用 | 内存不足时回收 | 缓存、图片缓存 | SoftReference<Object> |
| 弱引用 | 下次 GC 时回收 | 临时缓存、WeakHashMap | WeakReference<Object> |
| 虚引用 | 不影响回收,仅接收通知 | 对象销毁通知、资源清理 | PhantomReference<Object> |
💡 通俗解释 :
强引用就像自家房子 ------只要你不主动卖掉、不扔掉房产证,这房子永远是你的,就算全城房源紧张,也绝不会被收走。
软引用就像临时储物柜 ------商场人少(内存充足)时可以一直用;一旦人满为患、快要挤不下(快OOM),就会优先清空储物柜腾空间。
弱引用就像一次性纸巾 ------只要保洁一来打扫(GC触发),不管纸巾干不干净、有没有用,立刻就被收走。
虚引用就像快递取件通知------它不是快递本身,也不能用,只负责告诉你:这个快递(对象)已经被取走(回收)了。

4.3 代码示例:四种引用类型
java
import java.lang.ref.*;
import java.util.WeakHashMap;
public class ReferenceTypeDemo {
public static void main(String[] args) {
// 1. 强引用
Object strongObj = new Object();
// 只要 strongObj 不为 null,对象永远不会被回收
// 2. 软引用
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object softObj = softRef.get(); // 获取引用对象
// 内存不足时,softObj 会被回收,softRef.get() 返回 null
// 3. 弱引用
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object weakObj = weakRef.get(); // 获取引用对象
// 下次 GC 时,weakObj 会被回收,weakRef.get() 返回 null
// 4. 虚引用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// phantomRef.get() 始终返回 null
// 对象回收时,引用会被放入 queue
// 5. WeakHashMap(使用弱引用)
WeakHashMap<Object, String> weakMap = new WeakHashMap<>();
weakMap.put(new Object(), "value");
// key 是弱引用,GC 时会被回收
}
}
4.4 引用类型回收强度对比
┌─────────────────────────────────────────────────────────────────┐
│ 引用类型回收强度对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 强引用:████████████████████████████████████████ 永不回收 │
│ │
│ 软引用:████████████████████████ 内存不足时回收 │
│ │
│ 弱引用:████████ 下次 GC 时回收 │
│ │
│ 虚引用: 不影响回收(仅接收通知) │
│ │
│ 回收强度:强 > 软 > 弱 > 虚 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.5 实际应用场景
| 引用类型 | 应用场景 | 说明 |
|---|---|---|
| 强引用 | 普通业务对象 | 确保对象不被意外回收 |
| 软引用 | 图片缓存、数据缓存 | 内存充足时保留,不足时释放 |
| 弱引用 | WeakHashMap、临时缓存 | 不影响 GC,自动清理 |
| 虚引用 | 对象销毁通知、堆外内存清理 | 配合 ReferenceQueue 使用 |
软引用缓存示例
java
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;
public class SoftCacheDemo {
// 使用软引用实现缓存
private Map<String, SoftReference<Object>> cache = new HashMap<>();
public void put(String key, Object value) {
cache.put(key, new SoftReference<>(value));
}
public Object get(String key) {
SoftReference<Object> ref = cache.get(key);
if (ref != null) {
Object obj = ref.get();
if (obj != null) {
return obj; // 缓存命中
} else {
cache.remove(key); // 已被回收,移除缓存
}
}
return null; // 缓存未命中
}
}
✅ 面试金句 :
"四种引用类型提供了不同强度的引用关系,让开发者可以更精细地控制对象的生命周期。软引用适合缓存,弱引用适合临时对象,虚引用适合资源清理。"
五、不可达对象一定会被回收吗?
5.1 答案:不一定!
┌─────────────────────────────────────────────────────────────────┐
│ 不可达对象回收条件 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 不可达对象 → 第一次标记 → 是否有 finalize()? │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ 没有 finalize() 有 finalize() finalize() 已执行过 │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 直接回收 放入 F-Queue 直接回收 │
│ │ │
│ ▼ │
│ 执行 finalize() │
│ │ │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ 自救成功 自救失败 │
│ │ │ │
│ ▼ ▼ │
│ 移除标记 正式回收 │
│ (复活) │
│ │
│ 结论:不可达对象不一定会被回收,可能通过 finalize() 自救 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 自救的条件
| 条件 | 说明 |
|---|---|
| 重写了 finalize() 方法 | 对象必须重写 finalize() |
| finalize() 未被执行过 | 每个对象的 finalize() 只执行一次 |
| 在 finalize() 中重新建立引用 | 将 this 引用赋值给 GC Roots 可达的对象 |

5.3 代码验证
java
public class ResurrectDemo {
private static ResurrectDemo resurrectedObj = null;
@Override
protected void finalize() throws Throwable {
super.finalize();
// 自救:重新建立引用
resurrectedObj = this;
System.out.println("对象自救成功!");
}
public static void main(String[] args) throws InterruptedException {
ResurrectDemo obj = new ResurrectDemo();
obj = null; // 断开引用,对象不可达
// 第一次 GC
System.gc();
Thread.sleep(1000); // 等待 Finalizer 线程
if (resurrectedObj != null) {
System.out.println("对象还活着!✅"); // 输出
} else {
System.out.println("对象已死亡!");
}
// 断开自救后的引用
resurrectedObj = null;
// 第二次 GC
System.gc();
Thread.sleep(1000);
if (resurrectedObj != null) {
System.out.println("对象还活着!");
} else {
System.out.println("对象已死亡!✅"); // 输出(finalize() 只执行一次)
}
}
}
5.4 为什么 finalize() 不推荐使用?
| 问题 | 说明 |
|---|---|
| 性能差 | 需要额外线程执行,增加 GC 开销 |
| 行为不确定 | 执行时间不确定,可能延迟很久 |
| 只能执行一次 | 自救机会只有一次 |
| 可能复活对象 | 导致对象状态不一致 |
| 已废弃 | JDK 9 标记为 deprecated |
5.5 替代方案:Cleaner(基于虚引用)
java
import java.lang.ref.Cleaner;
public class CleanerDemo {
// 创建 Cleaner
private static final Cleaner cleaner = Cleaner.create();
// 需要清理的资源
private static class Resource {
@Override
protected void finalize() throws Throwable {
// 不推荐使用
}
}
// 清理动作
private static class CleanAction implements Runnable {
@Override
public void run() {
System.out.println("清理资源...");
}
}
public static void main(String[] args) {
Resource resource = new Resource();
// 注册清理动作
Cleaner.Cleanable cleanable = cleaner.register(resource, new CleanAction());
// 对象不可达时,CleanAction 会被执行
resource = null;
System.gc();
// 也可以手动清理
// cleanable.clean();
}
}
✅ 面试金句 :
"不可达对象不一定会被回收,可能通过 finalize() 自救。但 finalize() 已废弃,推荐使用 Cleaner 作为替代方案。"
六、面试回答模板 ------ 直接可用
6.1 标准回答(1-2 分钟)
面试官:如何判断对象可以被垃圾回收?
候选人:
Java 采用可达性分析算法判断对象是否可回收。
核心思想是从 GC Roots 开始向下搜索,无法到达的对象判定为可回收。
GC Roots 包括:
1. 虚拟机栈中引用的对象(局部变量、方法参数)
2. 方法区中静态属性引用的对象
3. 方法区中常量引用的对象
4. 本地方法栈 JNI 引用的对象
5. JVM 内部引用(类加载器、异常对象)
6. 被同步锁持有的对象
对象回收过程:第一次标记 → 执行 finalize() → 第二次标记 → 回收。
但 finalize() 在 JDK9 已废弃,不推荐使用。
另外,Java 有四种引用类型:强引用、软引用、弱引用、虚引用,
引用强度依次递减,回收时机也不同。

6.2 进阶回答(展现深度)
候选人:
(先说标准答案,然后补充)
关于对象回收判定,我想补充三点:
第一,Java 不采用引用计数法,因为无法解决循环引用问题。
可达性分析能准确判断对象的可达性,从根本上避免这个问题。
第二,不可达对象不一定会被回收。如果对象重写了 finalize()
且未被执行过,可以在 finalize() 中自救。但 finalize() 只能
执行一次,且 JDK9 已废弃,推荐使用 Cleaner 或 try-with-resources 来管理资源,它们更可控、更安全。。
第三,实际排查经验。我曾遇到过内存泄漏,通过 MAT 分析堆转储文件,
发现是静态集合类持有对象引用,导致对象无法被 GC Roots 断开。
这就是典型的强引用导致的内存泄漏问题。
另外,ThreadLocal 的不当使用也会导致内存泄漏,因为 ThreadLocalMap
的 Entry 继承 WeakReference,但 value 是强引用,需要及时 remove。

✅ 回答技巧:
- 先说可达性分析算法
- 列举 GC Roots 类型
- 补充 finalize() 和引用类型
- 结合项目经验(增加说服力)
七、得分要点与避坑指南
7.1 得分要点(必须覆盖)
| 维度 | 关键点 | 分值占比 |
|---|---|---|
| 判定方法 | 可达性分析(非引用计数) | 25% |
| GC Roots | 至少说出 4 种类型 | 30% |
| 回收过程 | 标记→finalize→二次标记→回收 | 20% |
| 引用类型 | 强软弱虚四种引用 | 15% |
| finalize() | 已废弃,推荐 Cleaner | 10% |
7.2 避坑指南(常见错误)
| 错误说法 | 正确理解 |
|---|---|
| "Java 使用引用计数法" | Java 使用可达性分析,引用计数法有循环引用问题 |
| "GC Roots 只有栈和静态变量" | GC Roots 有 6 种类型,要全面掌握 |
| "不可达对象一定会被回收" | 可能通过 finalize() 自救(虽然不推荐) |
| "finalize() 可以多次执行" | finalize() 每个对象只执行一次 |
| "虚引用可以获取对象" | 虚引用的 get() 始终返回 null |
7.3 加分项(展现深度)
- ✅ 能说出引用计数法的循环引用问题
- ✅ 了解 finalize() 的自救机制和废弃原因
- ✅ 知道四种引用类型的回收时机和应用场景
- ✅ 了解 Cleaner 作为 finalize() 的替代方案
- ✅ 能结合项目经验说明软引用/弱引用的使用

结语:对象死亡判定,GC 机制的基石
对象死亡判定是 JVM 垃圾回收的理论基础。理解可达性分析、GC Roots、引用类型,不仅能帮你顺利通过面试,更能让你:
- 理解 GC 原理(为什么这样设计)
- 排查内存泄漏(哪些引用导致对象无法回收)
- 合理使用引用(软引用缓存、弱引用临时数据)
"知其然,知其所以然"
理解对象死亡判定的设计初衷,才能真正掌握垃圾回收的精髓。

互动话题 :
你在项目中使用过软引用或弱引用吗?是什么场景?欢迎在评论区分享你的使用经验!