JVM面试题解,垃圾回收之“对象存活判断”剖析
一、JVM怎么判断一个类/对象是不是垃圾?
先来说如何判断一个对象是不是垃圾
最常用的就是引用计数法和可达性分析
引用计数法
引用计数法为每个对象维护一个计数器来跟踪有多少个引用指向该对象。每当创建一个新的引用指向某个对象时,计数器加1;每当一个引用失效或被移除时,计数器减1。当计数器降为0时,表示没有其他对象引用它,因此可以安全地将其回收。
引用计数法有个非常大的问题就是循环引用。
循环引用:如果两个或多个对象相互引用形成环状结构,即使这些对象不再被外部使用,它们的引用计数也不会降为0,从而导致内存泄漏。例如,A持有B的引用,B也持有A的引用,但没有任何其他对象引用A或B,这时它们应该被回收,但由于相互引用,它们的引用计数都不会降为0。
那么如果采用了引用计数法,如何解决循环引用呢? 引入弱引用来打破循环引用链。弱引用不会增加对象的引用计数,因此当一个对象仅通过弱引用保持时,它可以被垃圾回收器回收。
可达性分析
可达性分析就是用来判定对象是否存活的。
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是不可用的。
二、说到GC ROOTS,你知道Java中哪些对象可作为GC ROOTS吗?
作为 GC Roots 的对象包括下面几种(重点是前面 4 种):
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;各个线程调用方法堆栈中使用到的参数、局部变量、临时变量等。
- 方法区中类静态属性引用的对象;java 类的引用类型静态变量。
- 方法区中常量引用的对象;比如:字符串常量池里的引用。
- 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象。
- JVM 的内部引用( class 对象、异常对象 NullPointException 、 OutofMemoryError ,系统类加载器)。(非重点)
- 所有被同步锁( synchronized )持有的对象。(非重点)
- JVM 内部的 JMXBean 、 JVMTI 中注册的回调、本地代码缓存等(非重点)
- JVM 实现中的“临时性”对象,跨代引用的对象(在使用分代模型回收时只回收部分代的对象)(非重点)
除了这些固定的 GC Roots 集合以外,跟进用户选用的垃圾回收器以及当前回收的内存区域不同,还可能会有其他对象"临时"加入成为 GC Roots 。
三、对象不可达是不是立即被回收死亡?
即使通过可达性分析判断不可达的对象,也不是“非死不可”,它还会处于“缓刑”阶段,真正要宣告一个对象死亡,需要经过两次标记过程
- 一次是没有找到与 GCRoots 的引用链,它将被第一次标记。
- 随后进行一次筛选(如果对象覆盖了 finalize ),我们可以在 finalize 中去拯救,俗称对象的自我救赎。
finalize()方法的工作原理
-
标记阶段:当垃圾回收器开始运行时,它首先会进行一次可达性分析,找出所有不可达的对象。
-
Finalization队列:对于那些实现了
finalize()
方法且尚未被调用过finalize()
的不可达对象,JVM不会直接回收它们,而是将这些对象放入一个称为finalization队列的数据结构中。 -
调用
finalize()
:随后,一个专门的Finalizer线程会从finalization队列中取出对象,并调用它们的finalize()
方法。这为对象提供了一个最后的机会来执行清理代码或甚至重新与程序中的其他部分建立联系(虽然这种做法不推荐)。 -
第二次可达性分析:在
finalize()
方法执行完毕后,垃圾回收器会对这些对象再次进行可达性分析。如果此时这些对象仍然不可达,那么它们将会被真正地回收;否则,如果finalize()
方法使对象重新变得可达(例如通过将自身赋值给某个静态变量或全局变量),则该对象就不会被回收。