JVM--20-面试题6:如何判断对象可以被垃圾回收?

深入 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。

回答技巧

  1. 先说可达性分析算法
  2. 列举 GC Roots 类型
  3. 补充 finalize() 和引用类型
  4. 结合项目经验(增加说服力)

七、得分要点与避坑指南

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 原理(为什么这样设计)
  • 排查内存泄漏(哪些引用导致对象无法回收)
  • 合理使用引用(软引用缓存、弱引用临时数据)

"知其然,知其所以然"

理解对象死亡判定的设计初衷,才能真正掌握垃圾回收的精髓。


互动话题

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

相关推荐
蚊子码农2 小时前
每日一题--JVM线程分析与死锁排查
jvm
xuxie995 小时前
NEXT 1 进程2
java·开发语言·jvm
weisian1519 小时前
JVM--19-面试题5:说说JVM的类加载机制和双亲委派模型
jvm·双亲委派模型·jvm类加载机制
亓才孓10 小时前
【反射机制】
java·javascript·jvm
Volunteer Technology10 小时前
JVM之性能优化
jvm·python·性能优化
Andy Dennis11 小时前
Java语法注意事项
java·开发语言·jvm
坚持的小马11 小时前
JVM相关笔记-jps
jvm·笔记
昱宸星光11 小时前
Xnio源码分析
java·jvm·spring
@insist12312 小时前
软考-数据库系统工程师-计算机存储层次结构与性能优化核心知识点
大数据·jvm·数据库