可终结对象为什么需要至少经过两个垃圾回收周期才能被回收?

3
我正在阅读这篇文章,但是我并不完全理解最终化对象(覆盖finalize方法的对象)在可以被回收之前至少需要2个GC循环

在最好的情况下,需要至少两个垃圾回收循环才能回收可终止对象。

还有人能详细解释一下最终化对象为什么可能需要多个GC周期进行回收吗?

我的逻辑论点是,当我们重写finalize方法时,运行时将不得不向垃圾收集器注册这个对象(以便GC可以调用此对象的finalize方法),这使我认为GC将引用所有可终止对象。如果是这样,那么这个对象如何成为首先可以被GC回收的候选对象?我通过这个理论达到了矛盾。

PS:我知道覆盖finalize不是推荐的方法,自Java 9以来,该方法已被弃用。

2个回答

6
你说得对,垃圾回收器需要引用可终止对象。当然,在决定对象在终止之前是否仍然可达时,这个特定的引用不能被考虑。这意味着垃圾回收器对这个引用有特殊的了解。
当垃圾回收器确定一个对象符合终止条件时,将运行终止程序,这意味着该对象再次变为强可达状态,至少在执行终止程序期间是这样。在终止之后,对象必须再次变得不可达,并且必须检测到这一点,才能回收对象的内存。这就是为什么需要至少两个垃圾回收周期的原因。
在广泛使用的Hotspot/OpenJDK环境中(以及IBM的JVM中可能也是如此),这是通过创建一个特殊的、非公共子类Reference的实例来实现的,该子类称为Finalizer,当一个具有非平凡finalize()方法的对象被创建时就会创建该实例。与弱引用和软引用一样,当没有指向引用对象的强引用时,垃圾收集器会将这些引用入队列,但它们不会被清除,因此finalizer线程可以读取对象,使其再次变为强可达状态以进行最终处理。此时,Finalizer被清除,但也不再被引用,因此它将像普通对象一样被回收,因此在下一次引用对象变得不可达时,不再存在对它的特殊引用。
对于类具有“微不足道的终结器”的对象,即通过java.lang.Object继承的finalize()方法或空的finalize()方法,JVM将采取捷径并且不会首先创建Finalizer实例,因此您可以说,这些对象(构成所有对象的大部分)的行为就像它们的终结器已经从一开始就运行了。

谢谢您清晰的解释。我现在明白了为什么具有非平凡finalize的对象需要多于1个GC周期。但我仍然不明白为什么它们需要至少两个周期。您能解释一下吗? - Lavish Kothari
是的,我理解。但我想不通为什么它需要至少两个而不是恰好两个。基本上,我不明白何时可能需要超过2次GC循环的情况。您能否也解释一下这一点。 - Lavish Kothari
3
由于垃圾收集器不能保证识别特定的不可达对象,因此任何一般性陈述都必须小心谨慎,以避免承诺未必能够实现的内容。实际上,第二个(第三个、第四个等)垃圾收集周期可能会在终结器仍在运行时发生,因此对象仍然是可达的。事实上,在对象还在排队等待回收并且终结器尚未启动之前,第二个周期甚至可能已经发生。而且支持最大暂停时间的垃圾收集器可能无法在一个周期内识别所有不可达对象。 - Holger

0

虽然你已经得到了正确的答案,但我想在这里补充一点小的附言。一般来说,引用有两种类型:强引用弱引用。弱引用包括WeakReference/SoftReference/PhantomReferenceFinalizer

当某个GC周期遍历堆图并看到其中一个弱引用时,它会以特殊的方式处理它。当它第一次遇到一个死亡的finalizer引用(我们假设这是第一个GC周期),它必须使该实例复活。finalize是一个实例方法,它需要一个实际的实例才能被调用。因此,GC首先发现这个对象已经死亡,只是为了在片刻之后复活它,以便能够调用finalize。一旦它在上面调用了该方法,就标记了它已经被调用过;所以当下一个周期发生时,它可以被真正地GC掉。

把这称为第二个GC是不正确的。

例如,G1GC 对堆(年轻代和混合代)进行部分清理,因此它可能甚至不会在下一个周期中捕获此引用。就是这么简单,它可能不在其雷达范围内。
其他的垃圾收集器,如Shenandoah,有控制处理这些特殊引用的迭代次数的标志(默认为ShenandoahRefProcFrequency,为5)。
因此确实需要两个周期,但它们不必是连续的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接