【问题标题】:Java PhantomReference vs finalize()Java PhantomReference 与 finalize()
【发布时间】:2018-12-17 19:53:59
【问题描述】:

我一直在阅读这篇关于 PhantomReference https://www.baeldung.com/java-phantom-reference 的文章以及在那里找到的简化示例代码:

public static void main(String[] args) throws InterruptedException {
    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    Object object = new Object();
    PhantomReference<Object> phantomReference = new PhantomReference<>(object, referenceQueue);
    object = null;
    System.gc();
    Thread.sleep(1_000);
    System.out.println("isEnqueued() after GC: " + phantomReference.isEnqueued());
    Reference reference = referenceQueue.poll();
    if(reference != null) {
        System.out.println("isEnqueued() after poll(): " + phantomReference.isEnqueued());
    }
}

这是输出:

isEnqueued() after GC: true
isEnqueued() after poll(): false

所以一切都按预期工作,对对象的强引用设置为空,这被 GC 检测到,幻像引用被添加到队列中。

现在在那篇文章中他们说:“垃圾收集器在其引用对象的 finalize 方法执行后向引用队列添加一个幻像引用。这意味着该实例仍在内存中。”

所以我想做一个测试并覆盖 finalize 方法,例如:

Object object = new Object() {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize()");
    }
};

但是输出不同,幻影引用不再添加到队列中:

finalize()
isEnqueued() after GC: false

有人能解释一下为什么在这个更改之后输出不同以及如何更改这个代码以便将幻像引用添加到队列中吗?

我一直在 JDK 8 和 11 上对此进行测试,在两个平台上的结果相同。

【问题讨论】:

    标签: java reference garbage-collection phantom-reference


    【解决方案1】:

    语句“垃圾收集器在其引用对象的 finalize 方法执行后向引用队列添加一个幻像引用。”充其量是有点草率。

    你应该参考the specification:

    如果垃圾收集器在某个时间点确定幻像引用的引用对象是phantom reachable,那么在那个时候或以后的某个时间它将把引用排入队列。

    而“幻象可达”的链接定义状态:

    一个对象是幻象可到达的,如果它既不是强、软或弱可到达的,它已经被终结,并且一些幻象引用引用了它。

    因此,如果仅由幻像引用引用,则“在执行其所指对象的 finalize 方法后”对象是幻像可访问的,因此将在此之后但不会立即入队。由于一个对象在其finalize() 方法的执行期间是强可达的,因此至少需要一个额外的垃圾回收周期来检测它是否变为幻象可达。然后,“在那个时候或以后的某个时间”,它将被排队。

    如果你把程序改成

    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    Object object = new Object() {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalize()");
        }
    };
    PhantomReference<Object> phantomReference=new PhantomReference<>(object, referenceQueue);
    object = null;
    System.gc();
    Thread.sleep(1_000);
    System.gc();
    Thread.sleep(1_000);
    System.out.println("isEnqueued() after GC: " + phantomReference.isEnqueued());
    Reference reference = referenceQueue.poll();
    if(reference != null) {
        System.out.println("isEnqueued() after poll(): " + phantomReference.isEnqueued());
    }
    

    您很可能会看到所需的输出,但必须强调的是,不能保证垃圾收集器在您调用 System.gc() 时会实际运行,或者它在运行时会在特定时间内完成,或者它将在特定周期内找到所有无法访问的对象。此外,入队是在 gc 循环之后异步发生的,因此即使在垃圾收集器完成并检测到特殊的可达性状态时,在引用入队之前可能还要经过额外的时间。


    请注意句子“这意味着该实例仍在内存中”。也没有理解正确,但在这种情况下,它是基于一个甚至在 Java 核心开发人员方面的误解。

    创建 API 时,规范中添加了一句话,即使在 Java 8 版本中您也会发现:

    与软引用和弱引用不同,幻像引用在排队时不会被垃圾收集器自动清除。可以通过幻像引用访问的对象将保持不变,直到所有此类引用都被清除或自身变得不可访问。

    这可能会导致对象仍然必须在内存中的天真假设,但The Java® Language Specification 声明:

    可以设计优化程序的转换,将可到达的对象的数量减少到比那些天真地认为是可到达的要少。

    简单地说,如果程序的行为没有改变,对象的内存可能会更早地被回收。这尤其适用于应用程序根本无法使用对象的场景,例如幻像引用。如果对象不再在内存中,程序的行为就不会改变,所以你不能假设它确实存在。

    这导致了一个问题,为什么不清除虚引用的规则被添加到规范中。正如this answer 中所讨论的那样,这个问题被提出来了,根本无法回答。因此,此规则已在 Java 9 中删除,并且在入队时会清除幻像引用,例如弱引用和软引用。这是不假设对象仍在内存中的更强有力的理由,因为现在即使是非优化环境也可以在此时回收对象的内存。

    【讨论】:

    • 感谢非常详细的解释!我可以再问一下关于这句话的更多澄清:“垃圾收集器不会自动清除幻影引用,因为它们被排入队列”。所以这意味着在 Java 8 中对引用对象的强引用不为空(我正在查看 java.lang.ref.Reference 类的源代码)?从 Java 9 开始,在将幻像引用添加到队列后它是 null 吗?我想这只是技术上的差异,因为无法通过 get() 方法访问强引用,只是调用 clear() 方法在 Java 8 和 9 方面有一些区别?
    • 是的,这意味着referent字段保持非空,而get()方法强制总是返回null,这两个属性的组合确保了“一个可以通过幻影引用将保持如此”,尽管尚不清楚为什么这应该是一个实际目标。对于应用程序来说,这没有什么区别,因为引用对象不可访问。它甚至不会改变clear() 的行为,因为该方法只是将referent 字段设置为null,无论之前是否为null。只有 gc 不再需要额外的循环来回收内存。
    • 实际上至少Shenandoah 仍然会为PhantomReferences 本身可访问的referent 保持活动状态;仅在 java-9 之后它才停止这样做。我不能投票两次,但是一个很好的答案。
    猜你喜欢
    • 2017-09-04
    • 1970-01-01
    • 2013-11-18
    • 1970-01-01
    • 2011-03-20
    • 2011-03-19
    • 2015-03-13
    • 2010-09-06
    • 2013-09-20
    相关资源
    最近更新 更多