为什么在重写finalize方法时,引用不被放入引用队列中?

9
public class Test {
    public static void main(String[] args) throws Exception {
        A aObject = new A();

        ReferenceQueue<A> queue = new ReferenceQueue<>();
        PhantomReference<A> weak = new PhantomReference<>(aObject, queue);

        aObject = null;
        System.gc();

        TimeUnit.SECONDS.sleep(1);

        System.out.println(queue.poll());
    }
}

class A{
    @Override
    protected void finalize() throws Throwable {
        // TODO Auto-generated method stub
        super.finalize();

        System.out.println("finalize");
    }
}

结果是:
finalize
null

但如果我删除类A中的finalize方法,结果是:
java.lang.ref.PhantomReference@5b2c9e5d

因此,结果显示当我重写finalize方法时,弱对象没有放入引用队列中,这是因为aObject复活了吗?但我在finalize方法中什么都没做。


一些附加信息:取消注释super.finalize()或者System.out.println(...)并不会改变行为。取消整个方法的注释会导致期望的行为。将super.finalize()System.out.println(...)放置在if (false) { ... }中也会产生期望的行为,而将这两个语句放置在if (true) { ... }中则会产生意外的行为。在Oracle Java 1.8.0_151中进行了测试。也许这有点有用。 - Turing85
你确定这不是由于多线程环境中的熵所导致的吗?在循环中重复运行此测试是否总是产生相同的结果? - M. Prokhorov
@M.Prokhorov 刚刚进行了一些测试。在循环中重复代码不会改变结果(我将每个星座重复了100次)。该应用程序明确是单线程的。 - Turing85
@M.Prokhorov,就用户线程而言,它是单线程的。当然,JVM会产生其他线程,这是程序员无法控制的。 - Turing85
@M.Prokhorov 我可以编辑问题并添加我迄今收集的所有信息,但我觉得这就像从 OP 那里“偷走”问题。我自己正在等待 OP 的回复。 - Turing85
显示剩余2条评论
2个回答

4

有一个非平凡的finalize方法,Java在finalize运行之前就知道对象是不可达的,但它不知道对象在finalize之后仍然是不可达的。

它必须等待对象在另一个GC周期中再次被认为是不可达的,然后才能将虚引用加入队列。

*在java.lang.ref文档的术语中并非完全不可达,但也不是强可达、软可达或弱可达。


什么是“非平凡”的确切定义?让我困惑的是public void finalize() { super.finalize(); }似乎“太复杂”,但是public void finalize() { if (false) { super.finalize(); } }却可以接受。 - Turing85
1
@Turing85:我认为这取决于实现方式。JLS 只是说“为了效率,实现可以跟踪那些没有重写 Object 类的 finalize 方法或以微不足道的方式重写它的类。我们鼓励实现将这些对象视为具有未被覆盖的终结器,并按照 §12.6.1 中描述的方式更有效地对其进行终止。” - user2357112
@Turing85 public void finalize() { if (false) { super.finalize(); } } 很可能被编译器识别为包含死代码,并最终等同于无意义的空重写。 - Hulk
@ Hulk 我知道。但编译器也应该认识到 public void finalize() { super.finalize(); } 实际上是一个未被覆盖的方法。 - Turing85
@Turing85 编译器无法删除显式方法声明,因为这会影响该方法的可访问性(protected 方法 Object.finalize() 只能通过 super 调用由子类访问,重写的方法可以被同一包中的所有类访问,即使您没有使用 public)。因此,有一个已编译的方法,当然必须保留 super.finalize() 调用。相比之下,在编译时删除 if(false) { ... } 语句没有任何影响(除了使 finalize() 成为一个平凡的空方法)。 - Holger

3
非常有趣的观察。以下是正在发生的事情:
当类具有 非平凡(在 OP 情况下为非空)finalize 方法时,JVM 将创建一个 java.lang.ref.Finalizer(它是 Reference 的子类)对象,并将其指向我们的引用对象,在本例中为 A 对象。这反过来会防止 PhantomReference 将其排队,因为 A 已经被 Finalizer 引用。
我在 Java 1.8 中使用调试器观察到了这一点,并且也在此处详细描述了这一点。
@Turing85 的观察结果是可以预期的,因为当我们删除 finalize 方法内部的所有语句时,它变得 平凡 并且表现得就像没有 finalize 方法的任何类一样,并且不会被 Finalizer 引用。
更新:
问题是是否Finalizer会清除其对A的引用。JVM在随后的GC运行中确实会清除它,最终允许PhantomReference将A入队到其引用队列中。
例如,使用非平凡finalize方法运行下面的代码将从其引用队列中获得非空PhantomReference
public static void main(String[] args) throws Exception {
    A aObject = new A();

    ReferenceQueue<A> queue = new ReferenceQueue<>();
    PhantomReference<A> pr = new PhantomReference<>(aObject, queue);

    aObject = null;
    System.gc();

    TimeUnit.SECONDS.sleep(1);

    System.gc();

    TimeUnit.SECONDS.sleep(1);

    System.out.println( queue.poll() );
}

输出:

finalize 
java.lang.ref.PhantomReference@15db9742

A被Finalizer引用,那么引用何时会被清除? - gesanri
尽管通常没有必要,因为也没有人会保留对Finalizer的引用,所以在 runFinalizer方法 结束时显式地清除了 Finalizer. 当一个 Reference 对象没有被入队并且不可达时,它会像普通的 Java 对象一样被回收,并且不计算在引用总数中。 - Holger

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