4

我一直在阅读这篇关于 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

所以一切都按预期工作,对对象的强引用设置为 null ,由 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 上对此进行测试,两个平台上的结果相同。

4

1 回答 1

10

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

您应该参考规范

如果垃圾收集器在某个时间点确定幻影引用的所指对象是幻影可到达的,那么在那个时间或稍后的某个时间它将将该引用加入队列。

而“幻影可达”的链接定义指出:

如果一个对象既不是强、软或弱可达的,它是最终确定的,并且一些幻象引用引用它,它就是幻象可达的。

因此,如果仅由幻像引用引用,则对象“在执行其所指对象的 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 版本中您也会发现:

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

这可能会导致对象仍然必须在内存中的天真假设,但Java® 语言规范指出:

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

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

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

于 2018-12-18T08:27:52.193 回答