2

我决定在一个单独的主题中继续https://stackoverflow.com/a/41998907/2674303 。

让我们考虑以下示例:

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 行可以更改顺序。看起来它们是并行工作的。

最后一行有时会打印 2 个 gc 民意调查,有时会打印 3 个

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

附言

弱参考 java 文档:

假设垃圾收集器在某个时间点确定一个对象是弱可达的。那时,它将原子地清除对该对象的所有弱引用以及对通过强引用和软引用链可以访问该对象的任何其他弱可达对象的所有弱引用。同时它将声明所有以前的弱可达对象都是可终结的。在同一时间或稍后的某个时间,它会将那些注册到引用队列中的新清除的弱引用排入队列

PhantomReference java 文档:

假设垃圾收集器在某个时间点确定对象是幻影可达的。那时,它将自动清除对该对象的所有幻像引用以及对该对象可从中访问的任何其他幻像可访问对象的所有幻像引用。在同一时间或稍后的某个时间,它会将那些在引用队列中注册的新清除的幻像引用排入队列

区别对我来说不清楚

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

我从@Holger 那里得到了我的问题的答案:

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

一个对象是弱可达的,如果它既不是强可达也不是软可达,但可以通过遍历弱引用来达到。当对弱可达对象的弱引用被清除时,该对象就有资格进行终结。
一个对象是幻影可达的,如果它既不是强可达的,也不是软可达的,也不是弱可达的,它已经完成了,并且一些幻象引用引用了它

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

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

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.

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

  • 将 Weak/Soft 排入已注册的 ReferenceQueue 实例
  • 运行 finalize 方法

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

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

  • 运行 finalize 方法
  • 检查该对象仍然只有 phantomReachable(检查该对象在 finalize 方法执行期间没有复活)。并且只有当对象是 GC 时才会将幻像引用添加到 ReferenceQueue

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

坦率地说,我不明白根据添加到参考队列中以获取弱参考和软参考的区别。想法是什么?

4

2 回答 2

3

关键是包文档中“<em>phantom reachable”的定义:

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

大胆强调我的

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

这是JLS §12.6的结果:

为了提高效率,实现可能会跟踪不覆盖类 Object 的 finalize 方法的类,或者以简单的方式覆盖它。
...<br> 我们鼓励实现将此类对象视为具有未覆盖的终结器,并更有效地终结它们,如 §12.6.1 中所述。

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

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

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

​​​​

¹ 尽管在 Java 9 之前,这种安全性并不是万无一失的,因为幻像引用不会自动清除,并且深度反射会破坏整个概念。

于 2019-06-24T11:04:57.100 回答
1

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

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

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

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

于 2019-06-21T14:42:43.540 回答