【问题标题】:Why enqueuing of PhantomReference takes more GC cycles than WeakReference or SoftReference?为什么 PhantomReference 的入队比 WeakReference 或 SoftReference 需要更多的 GC 周期?
【发布时间】:2019-06-21 14:16:44
【问题描述】:

我决定在一个单独的主题中继续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 文档:

假设垃圾收集器在某个时间点确定 对象弱可达的时间。到时候就会 原子地清除对该对象的所有弱引用和所有弱引用 对任何其他弱可达对象的引用 可以通过一系列强引用和软引用访问对象。在 同时它会声明所有以前的weakly-reachable 要最终确定的对象。在同一时间或稍后的某个时间 将注册的那些新清除的弱引用排入队列 带参考队列

PhantomReference java 文档:

假设垃圾收集器在某个时间点确定 一个对象是幻影可达的。那时它会原子地 清除对该对象的所有幻像引用和所有幻像引用 到该对象所在的任何其他幻影可达对象 可达。在同一时间或稍后的某个时间,它将排队 注册的那些新清除的虚拟引用 参考队列

区别对我来说不是很清楚

P.S.(我们说的是具有非平凡 finalize 方法的对象)

@Holger 回答了我的问题:

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

一个对象是弱可达的,如果它既不强也不软 可达,但可以通过遍历弱引用来达到。当。。。的时候 对弱可达对象的弱引用被清除,该对象 有资格进行最终确定。
一个对象是幻影可达的,如果 它既不是强烈的,柔和的,也不是微弱的可达性,它已经 最终确定了,并且一些幻像引用了它

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

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

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中,它是否完成或完成并不重要不是。但看起来我的例子表明它真的很重要。

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

【问题讨论】:

  • 你不应该让问题以这种方式发展。除此之外,您插入的内容非常错误。当 finalize 方法还没有被调用时,对象不是幻象可达的。是否存在幻像引用与尚未执行(非平凡) finalize 方法的对象完全无关。并且垃圾收集器不会按顺序执行那些命名的操作。 您的代码会定期调用System.gc()。一般情况下,finalize()完成后什么都不会发生。
  • @Holger 代码很糟糕 - 我同意,但我想知道为什么它的行为就像我看到的那样。代码调用(尝试)gc 只是为了检测如果现在开始 GC 会发生什么。
  • @Holger 我在实践中看到,GC 仅在 finalize 方法终止后才将幻影可达引用添加到 ReferenceQueue 中,即使它很长。你不同意。让我们先讨论这一点。你为什么这么认为?
  • 我没有不同意。我说过,当finalize() 方法被调用时,对象被认为是最终确定的,但要幻像可达,它还必须是“既不强、不柔和、也不弱可达”。在实践中,这意味着方法必须完成,并且尤其是,这意味着对象不能逃逸。我从来没有说过不同的话。但是你一直忽略这一点。
  • 在某个时间点,“既不强也不软可及”的条件得到满足,因此弱引用被清除并移交给入队,但幻像引用没有,因为对象尚未最终确定。然后,调用finalize()方法,现在对象是finalized,但也是强可达的,所以第一个条件不满足,所以对象还是不是幻可达的。对象必须再次变得“既不强烈也不柔软可及”再次。正如我之前的评论中所展示的,这也适用于新创建的弱引用。

标签: java garbage-collection phantom-reference


【解决方案1】:

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

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

我的粗体强调

请注意,当我们删除 finalize() 方法时,幻像引用与弱引用一起被立即收集。

这是JLS §12.6的结果:

为了提高效率,实现可能会跟踪不覆盖类 Object 的 finalize 方法的类,或者以简单的方式覆盖它。

我们鼓励实现将此类对象视为具有未覆盖的终结器,并更有效地终结它们,如 §12.6.1 中所述。

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

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

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

​​

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

【讨论】:

  • 已经完成表示finalize方法调用完成了吗?
  • 对于非平凡的finalize() 方法,这意味着该方法已被调用(并且“既不强,……可达”意味着它的执行通常也已完成)。对于具有微不足道的finalize() 方法的对象,这可能意味着该方法永远不会被调用,因此最终确定将是它们的初始状态。
  • 实现只是将这些对象视为已经完成,从不将它们入队这里入队是指对 finalize 方法调用的队列?
  • 是的,我插入了一个澄清,因为没有归属的“入队”确实会产生误导。
  • 所以对于非平凡的情况 to be finalized 意味着我们开始调用 finalize 方法,即使需要 5 分钟,我们在开始调用后就认为对象已完成?
【解决方案2】:

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

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

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

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

【讨论】:

  • 那么与弱/软引用相比,只有在完成方法执行后才能将 PhantomReference 加入队列?是否指定了此行为?如果是,在哪里?
  • "幻像引用对象,在收集器确定它们的引用对象可能会被回收后排队。" 如果 finalizer 可以使用对象,则无法回收它们。
  • 我不确定我的英语,所以我可能会误解 smth,但我可以根据 javadoc 中的那个短语想象以下顺序: 1. Gc 检测到所指对象只能幻影到达并并行启动 2 个操作: a) 加入 ReferenceQueue b) 完成方法调用
  • @gstackoverflow finalizer 可以访问this,因此该对象是强可达的。从理论上讲,如果finalizer 没有在this 上执行涉及happens-before 的操作,那么当JVM 开始努力优化代码时,它可以重新排序读取并回收它下面的对象。在实践中finalizers 并不是假设这可能发生。在最终确定过程中,在硬 JITing 和依赖于竞争条件之后,调试所发生的事情所需要的乐趣是不光考虑的。
  • 无论如何我看不到对最初问题的直接回答。为什么 WeakReference 可以在 finalize 方法调用期间入队但 PhantomReference 不能?
猜你喜欢
  • 2011-12-03
  • 2011-03-20
  • 2010-09-22
  • 2010-09-24
  • 1970-01-01
  • 2011-09-25
  • 1970-01-01
  • 1970-01-01
  • 2019-08-20
相关资源
最近更新 更多