Java中如何标记对象已完成(以便finalize方法不会第二次调用)?

6
主要问题在主题中,但是让我展示一下我对Java最终化过程的看法,以便我可以问你更多的问题。
垃圾回收(GC)通过标记所有活动对象开始进行垃圾收集。 当所有可达对象都标记为“活动”时,所有其他对象都是不可达的。下一步是检查每个不可达对象,并确定它是否可以立即清除或者应该首先完成最终化。如果对象的finalize方法有一个主体,那么gc认为这个对象是可最终化的并且应该被最终化;如果对象的finalize方法具有空主体(protected void finalize(){}),则它是不可最终化的,并且可以立即被gc清除。(我对此的理解正确吗?)
所有可最终化的对象将被放入相同的队列中,以后逐个进行最终化。据我所知,一个可最终化的对象可能需要花费很长时间才能被放置到队列中,同时等待其轮到进行最终化。这可能会发生,因为通常只有一个名为Finalizer的线程正在从队列中获取对象并调用它们的finalize方法,当我们在某个对象的finalize方法中有一些耗时操作时,队列中的其他对象将等待很长时间才能被最终化。当对象被最终化时,它被标记为FINALIZED并从队列中删除。在下一次垃圾回收过程中,收集器将看到这个对象再次不可达,并且具有非空finalize方法(再次),因此该对象应该被放入队列中(再次)-但是它不会,因为收集器以某种方式看到该对象已被标记为FINALIZED。(这是我的主要问题:以什么方式这个对象被标记为FINALIZED,收集器如何知道这个对象不应再次进行最终化?)
3个回答

5
只要我们谈论HotSpot JVM...
对象本身不被标记为已完成。
每次创建新的finalize对象时,JVM会创建一个额外的FinalizerRef对象(这与弱引用/软引用/虚引用有些相似)。
一旦您的对象被证明无法通过强引用访问,特殊引用将处理此对象。您对象的FinalizerRef将被添加到finalizer队列中(这是链表,与其他引用类型相同)。
当finalizer线程从队列中消耗FinalizerRef时,它将将其空指针置为空(尽管线程将保持对对象的强引用,直到finalizer完成)。
一旦FinalizerRef被置为空,对象就再也无法进入finalizer队列了。
顺便说一下
您可以使用-XX:+PrintReferenceGC查看更多GC诊断JVM选项)在GC日志中查看首选项处理时间(和引用数量)。

谢谢。这正是我在寻找的东西。我调查了这些类Finalizer和FinalReference,现在我清楚地理解了它的工作原理。 - Anton Kasianchuk

1
我不知道实际的终结过程是如何工作的,但如果我必须这样做,我会这样做 - 在对象元数据中存储一个三态标志,告诉GC对象是否刚刚停止使用,需要运行终结器或可能被删除。您可能需要查看Java源代码以了解详细信息,但这应该是总体模式:
(在新建中)
object.metadata.is_finalized=NEEDS_FINALIZE;

(在gc中)
while ((object=findUnreachableObject())!=null) {
    if (object.metadata.is_finalized==NEEDS_FINALIZE) {
        if (hasNonNullBody(object.finalize)) {
            Finalizer.addForProcessing(object);
            object.metadata.is_finalized=IN_FINALIZER_QUEUE;
        } else {
            object.metadata.is_finalized=REMOVE_NOW;
        }
    }
    if (object.metadata.is_finalized==REMOVE_NOW) {
        // destroy the object and free the memory
    }
}

(在Finalizer中)
while ((object=getObjectForProcessing)!=null) {
    object.finalize();
    object.metadata.is_finalized=REMOVE_NOW;
}

1
问题在于现代GC中没有像findUnreachableObject这样的东西。你唯一能得到的是findReachableObject,将其移动到幸存者区域,并简单地忘记所有不可达的垃圾。由于大多数新对象都会早逝,因此这种方法更加高效。 - maaartinus

1

JVM将元数据存储在对象头中。任何具有子类finalize()方法的对象都会被调用,即使为空。将其放入队列中不需要很长时间,但它可能会在队列中等待很长时间。


我可以相信JVM可以修改对象头来标记它为FINALIZED。但是我进行了一些调查,发现在某些情况下,一个空的finalize()方法的对象在经过一次垃圾回收后就被移除了。我的意思是,如果这个对象被放入队列中以进行finalized操作,那么至少需要两次垃圾回收才能将其移除(第一次垃圾回收将对象放入队列,第二次垃圾回收将对象移除)。而当我在finalize()方法中添加了System.out.println("I'm finalized")时,这个对象在经过两次垃圾回收后被移除了。 - Anton Kasianchuk
@AntonKasyanchuk 你说的没错,JVM可能会简化这个过程,但我不会假设这总是情况。 - Peter Lawrey
1
规范并不要求,但强烈建议检测“微不足道的终结器”,包括空方法和仅由super.finalize()调用组成的方法(最终到达Object.finalize())...请参见JLS §12.6。类实例的终结,在末尾。 - Holger

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