为什么PhantomReference排队比WeakReference或SoftReference花费更多的GC周期?

时间:2019-06-21 14:16:44

标签: java garbage-collection phantom-reference

我决定在一个单独的主题中继续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周期。怎么解释呢?在文档中某处提到了(我找不到)吗?

P.S。

WeakReference Java文档:

  

假设垃圾收集器在   物体难以到达的时间。那时它将   原子清除该对象的所有弱引用和所有弱对象   引用任何其他弱可达对象,从中   可通过一系列强引用和软引用来访问对象。在   同时它将声明所有以前弱可及的   要完成的对象。在同一时间或以后   将排队那些已注册的新近清除的弱引用   有参考队列

PhantomReference Java文档:

  

假设垃圾收集器在某个时间点确定   一个对象是幻影可达的。那时它将原子地   清除对该对象的所有幻像引用以及所有幻像引用   到该对象所在的任何其他幻影可到达对象   可达的。在同一时间或更晚的时间它会进入   那些已向其注册的新清除的幻像引用   参考队列

对我来说,差异还不清楚

P.S.(我们谈论的是使用非平​​凡的finalize方法的对象)

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

他(没有性别歧视,但我想是的)向我指出了java doc,并注意到,与“软引用”和“弱引用”相比,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检测到该对象是弱/软可访问对象,它就会并行执行两个操作

  • 将弱/软队列入已注册的ReferenceQueue实例
  • 运行finalize方法

因此,要添加到ReferenceQueue中,对象是否已复活都没有关系。

但是对于PhantomReference,动作是不同的。一旦GC检测到该对象是“幻影可到达”,它将依次执行以下操作:

  • 运行finalize方法
  • 检查该对象仍仅是phantomReachable(检查该对象在完成方法执行期间未恢复)。并且只有当对象是GC时,才将幻影引用添加到ReferenceQueue中

但是@Holger说它已经完成,这意味着JVM启动了 finalize()方法调用,对于将PhantomReference添加到ReferenceQueue而言,它是否完成还是无关紧要。不。但是看起来像我的例子表明这确实很重要。

坦白说,我不了解根据添加到弱引用和软引用的RefernceQueue中的区别。是什么主意?

2 个答案:

答案 0 :(得分:2)

关键点是the package documentation中“ 幻影可达”的定义:

  
      
  • 如果对象既不是强力,柔弱也不是弱力可到达的,则它是幻影可到达的它已完成,并且某些幻像引用引用了它。
  •   

粗体强调我的

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

这是JLS §12.6的结果:

  

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

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

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

由于没有终结器的对象的弱引用和幻像引用之间在行为上没有区别,因此可以说,终结的存在及其复活对象的可能性是存在幻像引用的唯一原因。仅在可以安全地假定它不再复活时才能够执行对象的清理操作。

¹但是,在Java 9之前,这种安全性不是防弹的,因为幻象引用不会自动清除,并且允许深层反射来扭曲整个概念。

答案 1 :(得分:1)

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

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

那似乎正在发生什么事情?一旦一个对象很难被收集,它也是可以最终确定的。因此,WeakReference可以排入队列,并且对象可以在同一世界停止事件中排队等待最终确定。终结线程与ReferenceQueue线程(主线程)并行运行。因此,您可能会以任意顺序看到输出的前两行,总是(除非大为延迟),然后是第三行。

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