【面试专栏|JVM虚拟机】JVM垃圾回收入门:对象死亡判断的底层逻辑
🍃 予枫:个人主页
📚 个人专栏: 《Java 从入门到起飞》《读研码农的干货日常》《Java 面试刷题指南》
💻 Debug 这个世界,Return 更好的自己!
引言
作为Java程序员,JVM垃圾回收机制是面试绕不开的高频考点,而“如何判断对象死亡”更是基础中的基础。很多人只记结论却不懂底层逻辑,面试时被面试官追问就慌了阵脚。本文将从原理出发,拆解对象死亡的2种核心判断方式,补充面试高频追问,帮你吃透考点、轻松应答,建议收藏备用!
文章目录
一、为什么要判断对象死亡?
在JVM中,内存资源是有限的,尤其是堆内存(存储对象实例的核心区域)。如果创建的对象不再被使用,却一直占用内存,久而久之会导致内存泄漏,甚至触发OOM(OutOfMemoryError)异常,导致程序崩溃。
因此,JVM垃圾回收的核心前提的是:准确判断哪些对象已经“死亡”(不再被任何地方引用,失去使用价值) ,才能安全地回收其占用的内存,保证程序稳定运行。
简单来说:判断对象死亡,是垃圾回收的“前置操作”,也是避免内存浪费的关键。
二、对象死亡判断的2大核心机制
JVM判断对象死亡,主要依赖两种机制:引用计数法 和 可达性分析算法。其中,可达性分析算法是目前JVM主流采用的核心方式,而引用计数法因自身缺陷被淘汰,我们逐一拆解。
2.1 引用计数法(已淘汰)
原理
给每个对象分配一个“引用计数器”,当对象被引用一次,计数器值加1;当引用失效(比如变量赋值为null),计数器值减1。当计数器值为0时,认为该对象已经死亡,可被回收。
示例(伪代码)
// 创建对象,计数器=1Object obj =newObject();// 新增引用,计数器=2Object obj2 = obj;// 引用失效,计数器=1 obj =null;// 引用失效,计数器=0(对象可被回收) obj2 =null;致命缺陷:循环引用问题
引用计数法最大的问题的是无法解决“循环引用”场景,这也是它被JVM淘汰的核心原因。
比如两个对象互相引用,但都不再被其他地方引用,此时它们的计数器值都为1,无法被回收,导致内存泄漏:
classA{privateB b;publicvoidsetB(B b){this.b = b;}}classB{privateA a;publicvoidsetA(A a){this.a = a;}}// 循环引用A a =newA();B b =newB(); a.setB(b); b.setA(a);// 引用失效,但计数器都为1,无法回收 a =null; b =null;正因为这个缺陷,目前主流JVM(如HotSpot)均未采用引用计数法,而是选择了可达性分析算法。
2.2 可达性分析算法(主流核心)
原理
以“GC Roots”(垃圾回收根节点)为起点,向下遍历所有引用链(引用关系)。如果一个对象没有任何一条引用链连接到GC Roots,则认为该对象不可达,标记为“可回收对象”(并非立即死亡,还需经过二次标记)。
简单来说:GC Roots是“不会被回收”的核心对象,从这些对象出发,能找到的对象都是“存活”的,找不到的就是“待回收”的。
第一步:明确GC Roots的组成(面试高频考点)
GC Roots必须是“绝对不会被垃圾回收”的对象,主要包括以下4类(记牢,面试常问):
- 虚拟机栈(栈帧中的局部变量表)中引用的对象(比如方法内定义的局部变量);
- 方法区中类静态属性引用的对象(比如static修饰的变量);
- 方法区中常量引用的对象(比如final修饰的常量);
- 本地方法栈中JNI(Native方法)引用的对象。
第二步:可达性分析流程(附流程图)
GC Roots
对象1
对象2
对象3
对象4
对象5
对象6
对象5、6无引用链连接到GC Roots,标记为可回收
流程拆解:
- 确定GC Roots集合(上述4类对象);
- 从GC Roots出发,遍历所有引用关系,形成“存活对象引用链”;
- 未被引用链覆盖的对象,标记为“不可达对象”,进入待回收队列。
第三步:对象死亡的最终确认(二次标记)
不可达对象并非立即死亡,还需经过“二次标记”才能确认死亡:
- 第一次标记:通过可达性分析,标记为不可达对象;
- 筛选判断:检查该对象是否重写了finalize()方法,且该方法未被执行过;
- 若未重写finalize(),或已执行过,则直接标记为“死亡对象”;
- 若重写了且未执行,则将对象放入“F-Queue”队列,由虚拟机自动执行finalize()方法;
- 二次标记:执行完finalize()方法后,再次判断对象是否可达。若仍不可达,则正式标记为死亡对象;若重新变得可达(比如在finalize()中重新建立引用),则移除待回收队列,继续存活。
⚠️ 注意:finalize()方法是对象“最后的自救机会”,但实际开发中不建议使用(执行时机不确定、效率低,还可能导致内存泄漏),JDK9已标记为过时方法。
三、面试官追问环节(实战价值拉满)
这部分是面试加分项,帮你提前应对面试官的连环追问,比纯记八股更有用!
追问1:JVM为什么不采用引用计数法,而是选择可达性分析算法?
答:核心原因是引用计数法无法解决“循环引用”问题,会导致内存泄漏;而可达性分析算法通过GC Roots为起点,能完美避开循环引用的坑(循环引用的对象无法连接到GC Roots,会被标记为不可达)。此外,可达性分析算法的判断逻辑更严谨,能更准确地识别存活对象。
追问2:GC Roots具体包含哪些对象?有没有补充的?
答:核心是4类(虚拟机栈局部变量、方法区静态属性、方法区常量、本地方法栈JNI引用),补充2类特殊情况:
- 被同步锁(synchronized)持有的对象;
- JVM内部的对象(比如类加载器、系统类、常驻内存的对象)。
追问3:对象被标记为不可达后,为什么不立即回收?
答:为了给对象一次“自救”的机会,即通过finalize()方法重新建立引用,避免误回收(虽然实际开发中很少用,但JVM设计时保留了这个机制)。同时,批量回收不可达对象能提高垃圾回收的效率,避免频繁触发回收操作。
追问4:finalize()方法能真正让对象“自救”吗?为什么不推荐使用?
答:理论上可以(在finalize()中给对象重新赋值,建立到GC Roots的引用),但实际不推荐,原因有3点:
- 执行时机不确定:finalize()由JVM的Finalizer线程执行,线程优先级低,无法保证及时执行;
- 效率低:Finalizer线程执行速度慢,大量使用会拖慢垃圾回收效率;
- 易导致内存泄漏:若finalize()中出现异常,对象无法被回收,长期积累会导致内存泄漏。
四、总结
本文核心讲解了JVM判断对象死亡的2种机制,重点掌握可达性分析算法(GC Roots、引用链遍历、二次标记),这是面试的核心考点。
核心要点总结:
- 判断对象死亡是垃圾回收的前提,避免内存泄漏和OOM;
- 引用计数法因循环引用缺陷被淘汰;
- 可达性分析算法是主流,核心是GC Roots和引用链遍历;
- 不可达对象需经过二次标记,finalize()可自救但不推荐使用。
建议结合本文的面试追问,反复梳理原理,面试时才能从容应答。觉得有用的话,点赞+收藏,后续持续更新JVM面试干货!