为什么 PhantomReference 的入队比 WeakReference 或 SoftReference 需要更多的 GC 周期?

Why enqueuing of PhantomReference takes more GC cycles than WeakReference or SoftReference?

我决定在另一个主题中继续

让我们考虑以下示例:

public class SimpleGCExample {
    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> queue=new ReferenceQueue<>();
        SimpleGCExample e = new SimpleGCExample();
        Reference<Object> pRef=new PhantomReference<>(e, queue),
                wRef=new WeakReference<>(e, queue);
        e = null;
        for(int count=0, collected=0; collected<2; ) {
            Reference ref=queue.remove(100);
            if(ref==null) {
                System.gc();
                count++;
            }
            else {
                collected++;
                System.out.println((ref==wRef? "weak": "phantom")
                        +" reference enqueued after "+count+" gc polls");
            }
        }
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalizing the object in "+Thread.currentThread());
        Thread.sleep(100);
        System.out.println("done finalizing.");
    }
}

Java 11 次打印如下:

finalizing the object in Thread[Finalizer,8,system]
weak reference enqueued after 1 gc polls
done finalizing.
phantom reference enqueued after 3 gc polls

前两行可以改变顺序。看起来他们是并行工作的。

最后一行有时打印 2 次 gc 轮询,有时打印 3 次

所以我看到 PhantomReference 的入队需要更多的 GC 周期。怎么解释呢?它是否在文档中的某个地方提到过(我找不到)?

P.S.

WeakReference java 文档:

Suppose that the garbage collector determines at a certain point in time that an object is weakly reachable. At that time it will atomically clear all weak references to that object and all weak references to any other weakly-reachable objects from which that object is reachable through a chain of strong and soft references. At the same time it will declare all of the formerly weakly-reachable objects to be finalizable. At the same time or at some later time it will enqueue those newly-cleared weak references that are registered with reference queues

PhantomReference java 文档:

Suppose the garbage collector determines at a certain point in time that an object is phantom reachable. At that time it will atomically clear all phantom references to that object and all phantom references to any other phantom-reachable objects from which that object is reachable. At the same time or at some later time it will enqueue those newly-cleared phantom references that are registered with reference queues

我不清楚区别

P.S。(我们正在谈论具有非平凡终结方法的对象)

@Holger 回答了我的问题:

他(没有性别歧视,但我想是)向我指出 java doc 并注意到 PhantomReference 与软引用和弱引用相比包含额外的短语:

An object is weakly reachable if it is neither strongly nor softly reachable but can be reached by traversing a weak reference. When the weak references to a weakly-reachable object are cleared, the object becomes eligible for finalization.
An object is phantom reachable if it is neither strongly, softly, nor weakly reachable, it has been finalized, and some phantom reference refers to it

我的下一个问题是关于它是什么意思它已经完成我预计这意味着完成方法已完成

为了证明这一点,我像这样修改了应用程序:

public class SimpleGCExample {
    static SimpleGCExample object;

    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Object> queue = new ReferenceQueue<>();
        SimpleGCExample e = new SimpleGCExample();
        Reference<Object> pRef = new PhantomReference<>(e, queue),
                wRef = new WeakReference<>(e, queue);
        e = null;
        for (int count = 0, collected = 0; collected < 2; ) {
            Reference ref = queue.remove(100);
            if (ref == null) {
                System.gc();
                count++;
            } else {
                collected++;
                System.out.println((ref == wRef ? "weak" : "phantom")
                        + " reference enqueued after " + count + " gc polls");
            }
        }
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalizing the object in " + Thread.currentThread());
        Thread.sleep(10000);
        System.out.println("done finalizing.");
        object = this;
    }
}

我看到以下输出:

weak reference enqueued after 1 gc polls
finalizing the object in Thread[Finalizer,8,system]
done finalizing.

并且应用程序挂起。我认为这是因为对于 Weak/Soft 引用,GC 按以下方式工作:一旦 GC 检测到该对象 Weak/Soft 可访问,它就会并行执行 2 个操作

因此,对于添加到 ReferenceQueue 中,对象是否复活并不重要。

但对于 PhantomReference 操作是不同的。一旦 GC 检测到该对象是 Phantom Reachable,它就会按顺序执行以下操作:

但是@Holger说它已经完成了意味着JVM发起了finalize()方法调用并将PhantomReference添加到ReferenceQueue中完成与否并不重要。但看起来我的例子表明它真的很重要。

坦率地说,我不明白为弱引用和软引用添加到 ReferenceQueue 中的区别。想法是什么?

PhantomReferences 只会在任何关联的 finalizer 完成执行后入队。请注意 finalizer 可以复活一个对象(普林斯顿以前的安全互联网项目使用效果很好)。

未指定超出规范的确切行为。这里是实现相关的东西。

那么似乎发生了什么? 一旦一个对象是弱可收集的,它也是可终结的。因此,WeakReference 可以入队,并且对象可以在同一个 stop-the-world 事件中排队等待完成。最终线程是 运行 与您的 ReferenceQueue 线程(主线程)并行。因此,您可能会以任一顺序看到输出的前两行,总是(除非严重延迟)然后是第三行。

仅在您的 finalizer 退出一段时间后,PhantomReference 才可入队。因此 gc 计数严格更大。该代码看起来像是一场相当公平的比赛。也许改变毫秒超时会改变事情。大多数事情 GC 没有确切的保证。

重点是the package documentation中“phantom reachable”的定义:

  • An object is phantom reachable if it is neither strongly, softly, nor weakly reachable, it has been finalized, and some phantom reference refers to it.

大胆强调我的

请注意,当我们删除 finalize() 方法时,会立即收集虚引用以及弱引用。

这是JLS §12.6的结果:

For efficiency, an implementation may keep track of classes that do not override the finalize method of class Object, or override it in a trivial way.

We encourage implementations to treat such objects as having a finalizer that is not overridden, and to finalize them more efficiently, as described in §12.6.1.

不幸的是,§12.6.1 没有讨论“拥有未被覆盖的终结器”的后果,但很容易看出,该实现只是将这些对象视为已经完成,从不将它们排队等待最终确定,因此能够立即回收它们,这会影响典型 Java 应用程序中的大多数对象。

另一种观点是确保 finalize() 方法最终被调用的必要步骤,即 Finalizer 实例的创建和链接,对于具有琐碎的终结者。此外,在逃逸分析后消除纯本地对象的创建,仅适用于这些对象。

由于没有终结器的对象的弱引用和幻像引用之间没有行为差异,我们可以说终结器的存在及其复活对象的可能性是幻影引用存在的唯一原因,仅当可以安全地假设对象无法再复活时才能够执行对象清理¹。

¹ 虽然,在 Java 9 之前,这种安全性并不 bullet-proof,因为幻影引用不会自动清除,深度反思允许扭曲整个概念。