[面试题] JVM 如何判断对象是否存活?
📖 版块 A:知识讲解版 (温故知新)
-
引用计数算法 (Reference Counting):
- 原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器加 1;引用失效时,计数器减 1。
- 优缺点:实现简单,判定效率高;但无法解决对象之间“循环引用”的问题,因此主流 JVM 均不采用此方案。
-
可达性分析算法 (Reachability Analysis):
- 原理:通过一系列被称为 “GC Roots” 的根对象作为起始节点,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”。
- 结论:如果一个对象到 GC Roots 没有任何引用链相连(即不可达),则证明此对象不再被使用。
-
哪些可以作为 GC Roots:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI(即 Native 方法)引用的对象。
- JVM 内部的引用(如系统类加载器、异常对象等)。
-
对象死亡的二次标记: 不可达对象并非“非死不可”,它会经历两次标记过程:
- 第一次标记:筛选出没有与 GC Roots 相连的对象。
- 第二次标记:如果对象覆盖了
finalize()方法且未被系统调用过,它会被放入 F-Queue 队列中由低优先级线程触发finalize()。若对象在此时重新与引用链建立关联,则可“自救”成功,否则会被回收。
🎤 版块 B:面试实战版 (高手逐字稿)
Q:JVM 如何判断对象是否存活?
💬 高手逐字稿:
关于 JVM 判断对象是否存活,主要有两种算法,但目前主流的虚拟机(比如 HotSpot)采用的是可达性分析算法。
首先,我简单提一下引用计数法。它的逻辑是给对象维护一个计数器,引用加一,失效减一。虽然它效率很高,但有一个致命缺陷,就是无法解决循环引用的问题。比如 A 引用 B,B 也引用 A,但它们都没被外部引用,这时候计数器永远不为 0,导致内存泄露。所以 JVM 舍弃了这种方案。
现在常用的可达性分析算法,核心思想是通过一系列被称为 GC Roots 的根对象作为起点,沿着引用链向下搜索。如果一个对象到 GC Roots 之间没有任何引用链,就说明它是不可达的,会被标记为可回收对象。
这里有个细节,面试中经常被问到哪些对象能作为 GC Roots。我总结主要有四类:
- 第一是虚拟机栈里引用的对象,也就是我们方法运行时的局部变量。
- 第二是方法区中静态属性或常量引用的对象。
- 第三是 Native 方法(JNI)引用的对象。
- 第四是 JVM 内部持有的引用,比如系统类加载器或者一些异常对象。
自问自答环节: 这里其实还有一个比较深入的点,就是对象被判定为“不可达”后,是否就立刻死亡了呢?其实不一定。对象还有一次“自救”的机会。如果这个对象重写了 finalize() 方法,并且这是它第一次被标记回收,JVM 会把它放入一个低优先级的队列去执行 finalize()。如果它在这个方法里把自己赋值给了某个类变量或者成员变量,那么它就成功自救了,不会被回收。当然,在实际开发中,我们非常不建议依赖 finalize(),因为它运行代价高且不确定性太大。
最后我想补充的是,判断存活还跟引用强度有关。Java 把引用分成了强、软、弱、虚四种。如果是强引用,哪怕 OOM 也不回收;如果是软引用,在内存不足时会回收;如果是弱引用,下次 GC 只要扫到就会回收。所以在评估对象是否存活时,引用类型的权衡也是一个重要的考量因素。