【问题标题】:why the reference don't put into reference queue when finalize method overrided为什么在覆盖 finalize 方法时引用不放入引用队列
【发布时间】:2018-06-18 11:04:16
【问题描述】:
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 测试。也许这有点用处。
  • 您确定这不是由多线程环境中的熵引起的吗?在循环中重复运行此测试是否总是产生相同的结果?
  • @M.Prokhorov 刚刚做了一些测试。在循环中重复代码不会改变结果(我对每个星座重复了 100 次)。该应用程序绝对是单线程的。
  • @Turing85,这与 Javadoc 相矛盾,Javadoc 声明 gc(以及最终的终结器)在与用户启动的线程不同的线程中运行。
  • @M.Prokhorov 它是单线程 w.r.t。用户线程。当然,JVM 确实会产生不受程序员控制的其他线程。

标签: java reference finalize


【解决方案1】:

非常有趣的观察。这是正在发生的事情:

当类有 non-trivial(在 OP 情况下非空)finalize 方法时,JVM 将创建一个 java.lang.ref.Finalizer(它是 Reference 的子类)对象并将其指向我们的所指对象,在本例中为 A目的。这反过来又会阻止 PhantomReference 将其加入队列,因为 A 已被 Finalizer 引用。

这是我在使用 Java 1.8 的调试器中的观察者,也详细描述了here

@Turing85 的观察结果是意料之中的,因为当我们删除 finalize 方法中的所有语句时,它变为 trivial,并且其行为与没有 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引用了,那么什么时候清除引用呢?
  • Finalizerthe runFinalizer method 的末尾被明确清除,尽管这通常甚至没有必要,因为无论如何没有人会保留对Finalizer 的引用,所以它只是之后收集垃圾。当 Reference 对象未入队且不可访问时,它会像普通 Java 对象一样被收集,根本不算作引用。
【解决方案2】:

有了不平凡的finalize,Java 在finalize 运行之前就知道对象不可访问*,但它不知道在finalize 之后对象仍然不可访问。

它必须等待对象在另一个 GC 周期中再次被视为不可访问,然后才能将幻像引用入队。

*在java.lang.ref docs 的术语中不是完全无法到达,但不是强、柔和或弱可到达

【讨论】:

  • 究竟什么才是“不平凡的”?让我失望的是public void finalize() { super.finalize(); } 似乎“太复杂”,但public void finalize() { if (false) { super.finalize(); } } 很好。
  • @Turing85:我相信这取决于实施。 JLS 只说“为了提高效率,实现可能会跟踪不覆盖类 Object 的 finalize 方法的类,或者以微不足道的方式覆盖它。我们鼓励实现将此类对象视为具有不具有终结器的终结器。被覆盖,并更有效地完成它们,如 §12.6.1 中所述。"
  • @Turing85 public void finalize() { if (false) { super.finalize(); } } 几乎肯定会被编译器识别为包含死代码,并最终等同于平凡的空覆盖。
  • @Hulk 我知道。但编译器也应该认识到public void finalize() { super.finalize(); } 实际上是一个不可覆盖的方法。
  • @Turing85 编译器无法删除显式方法声明,因为这会影响该方法的可访问性(protected 方法 Object.finalize() 只能由子类通过 super 访问调用时,同一个包中的所有类都可以访问被覆盖的方法,即使您没有使用public)。所以有一个编译的方法,当然,必须保留super.finalize() 调用。相比之下,在编译时删除 if(false) { …} 语句没有任何影响(除了使 finalize() 成为一个微不足道的空方法)。
猜你喜欢
  • 1970-01-01
  • 2012-07-16
  • 1970-01-01
  • 1970-01-01
  • 2014-04-28
  • 1970-01-01
  • 1970-01-01
  • 2019-12-07
  • 2013-04-25
相关资源
最近更新 更多