JVM 如何判断对象是否存活的?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
面试官提出这个问题,通常期望考察以下几个方面:
- 对 JVM 垃圾回收基础的理解:是否清楚垃圾回收的第一步就是 “标记”,即识别哪些对象是 “垃圾”,哪些是 “存活” 的。
- 对主流算法的掌握:是否了解引用计数法 和 可达性分析算法 的核心思想,以及各自的优缺点。
- 对 GC Roots 的认知:是否清楚在可达性分析中,哪些对象可以作为 GC Roots,这直接关系到分析的准确性。
- 对 Java 引用的理解:是否知道强引用、软引用、弱引用、虚引用在判断对象存活时的不同作用,以及它们在缓存、内存敏感场景下的应用。
- 对对象“自救”机制的了解:是否知道
finalize()方法的存在、作用以及它的局限性和不推荐使用的原因。 - 实际调优意识:能否将理论联系到实际开发,比如如何利用不同引用类型优化内存使用,避免内存泄漏。
核心答案
JVM 判断对象是否存活,最核心的算法是 可达性分析。
简单来说,就是从一组被称为 GC Roots 的根对象出发,通过引用链向下搜索。如果一个对象到任何一个 GC Roots 都没有任何引用链相连(即从 GC Roots 到这个对象是不可达的),那么这个对象就被判定为“可回收”的。
虽然引用计数法也是一种判断方式,但由于它无法解决对象之间循环引用的问题,主流的 Java 虚拟机(如 HotSpot)都没有采用它。
深度解析
原理/机制:可达性分析的工作流程
可达性分析算法的核心是找到所有的 “活对象”,剩下的自然就是 “死对象”。这个过程就像在一个有向图中,从一组根节点出发进行遍历,所有被访问到的节点标记为“存活”,未被访问到的节点就是“可回收”的。
那么,哪些对象可以作为 GC Roots 呢?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象:比如当前正在执行的方法中,使用的局部变量、参数等所引用的对象。
- 方法区中类的静态属性引用的对象:比如一个类的静态成员变量(
static字段)引用的对象。 - 方法区中常量引用的对象:比如字符串常量池(
String Table)中的引用。 - 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
- Java 虚拟机内部的引用:如基本数据类型对应的 Class 对象、常驻的异常对象(NullPointerException、OutOfMemoryError 等)、系统类加载器。
- 所有被同步锁(
synchronized)持有的对象。 - 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
引用的类型与对象的 “生死”
判断对象是否存活,光靠可达性分析还不够,还需要考虑引用的强度。Java 提供了四种引用类型,它们在垃圾回收时的表现不同,直接影响了对象的 “存活” 判定。
- 强引用(Strongly Reference)
- 最常见的引用,如
Object obj = new Object()。 - 只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 最常见的引用,如
- 软引用(Soft Reference)
- 用于描述一些还有用,但非必需的对象。
- 在系统将要发生内存溢出异常之前,会把只被软引用关联的对象列进回收范围进行第二次回收。如果这次回收后内存仍然不足,才会抛出 OOM。
- 应用场景:适合实现内存敏感的缓存,比如图片缓存、网页缓存。
- 弱引用(Weak Reference)
- 用于描述非必需对象,强度比软引用更弱。
- 被弱引用关联的对象只能生存到下一次垃圾收集发生为止。无论当前内存是否足够,垃圾收集器工作时,都会回收只被弱引用关联的对象。
- 应用场景:
WeakHashMap、ThreadLocal中的ThreadLocalMap.Entry。
- 虚引用(Phantom Reference)
- 最弱的引用关系。为一个对象设置虚引用的唯一目的,就是能在这个对象被收集器回收时收到一个系统通知。
- 无法通过虚引用来取得一个对象实例。
对象的 “自我救赎” ——finalize() 方法
即使在可达性分析中,一个对象被判定为不可达,它也不是“非死不可”的。要真正宣告一个对象死亡,至少要经历两次标记过程:
- 第一次标记:如果对象在进行可达性分析后发现没有与 GC Roots 相连的引用链,它会被第一次标记。
- 筛选与第二次标记:随后,JVM 会判断该对象是否有必要执行
finalize()方法。如果对象没有覆盖finalize()方法,或者finalize()已经被 JVM 调用过了,那么 JVM 会认为“没有必要执行”,对象将直接回收。 - “自救”机会:如果对象被判定为有必要执行
finalize()方法,那么该对象会被放入一个名为F-Queue的队列中,稍后由一条由 JVM 自动建立的、低优先级的 Finalizer 线程去执行。如果在finalize()方法中,对象与引用链上的任何一个对象建立了关联(例如把自己this赋值给某个类变量或成员变量),那么它在第二次标记时就会被移出“即将回收”的集合,实现自救。如果finalize()执行后,对象仍然没有与任何引用链建立关联,那它就会被回收。
重要提示:finalize() 方法在 Java 9 中已被标记为弃用。因为它运行代价高昂,不确定性大,无法保证各个对象的调用顺序,我们完全可以通过 try-finally 或其他方式更好地管理资源释放。永远不要依赖 finalize() 来释放关键资源或实现业务逻辑。
代码示例:软引用 vs 弱引用
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class ReferenceTest {
public static void main(String[] args) {
// 创建一个强引用对象
Object strongObj = new Object();
System.out.println("强引用对象: " + strongObj);
// 创建一个软引用
SoftReference<Object> softRef = new SoftReference<>(new Object());
System.out.println("软引用对象: " + softRef.get());
// 创建一个弱引用
WeakReference<Object> weakRef = new WeakReference<>(new Object());
System.out.println("弱引用对象(GC前): " + weakRef.get());
// 主动触发垃圾回收
System.gc();
// 再次尝试获取弱引用对象(大概率已被回收)
System.out.println("弱引用对象(GC后): " + weakRef.get());
// 此时,强引用依然存在,软引用在内存充足时也依然存在
System.out.println("强引用对象: " + strongObj);
System.out.println("软引用对象: " + softRef.get());
}
}
输出结果(在不同 JVM 参数和环境下可能略有差异,但弱引用被回收是大概率事件):
强引用对象: java.lang.Object@15db9742
软引用对象: java.lang.Object@6d06d69c
弱引用对象(GC前): java.lang.Object@4e25154f
弱引用对象(GC后): null
强引用对象: java.lang.Object@15db9742
软引用对象: java.lang.Object@6d06d69c
常见误区
- 误区一:认为引用计数法是主流算法。很多初学者会答引用计数法,但面试官更希望你指出它的循环引用缺陷,并引出主流的可达性分析。
- 误区二:混淆四种引用的回收时机。特别是软引用和弱引用,容易记反。记住:软引用是在 OOM 之前才回收,弱引用是只要 GC 就回收。
- 误区三:过度依赖
finalize()。甚至有人试图用它来做资源清理,这是非常不可靠且过时的做法。 - 误区四:认为 GC Roots 就是栈里的引用。GC Roots 是一组引用的集合,不仅仅是栈,还包括静态变量、常量、JNI 引用等。
总结
JVM 通过可达性分析算法,以一组确定性的 GC Roots 为起点,遍历对象图,不可达的对象即为 “垃圾”。同时,结合强、软、弱、虚四种引用类型,精细化地控制对象的回收时机,而 finalize() 方法仅提供一次不太可靠的“自救”机会,在实际开发中应避免使用。