【问题标题】:Is it safe to have a PhantomReference to `this`?对“this”有一个 PhantomReference 是否安全?
【发布时间】:2019-06-02 11:11:39
【问题描述】:

我有一个共享资源,我想知道有多少其他对象仍在使用该资源。为此,我想使用PhantomReferences。

由于ReferenceQueues 不跟踪为它们注册的引用(source,“通知”部分),我的想法是将引用存储为被跟踪对象的字段:

class Foo {
    private PhantomReference<Foo> thisReference;

    public Foo(ReferenceQueue<Foo> queue) {
        thisReference = new PhantomReference<>(this, queue);
    }
}

这是否安全,基于 PhantomReferences 的 Java 9(+) 行为,或者是否有可能在没有将引用添加到队列的情况下对实例进行垃圾收集?

文档说:

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

但它没有提到垃圾收集是否可以在引用入队之前发生。

【问题讨论】:

    标签: java phantom-reference


    【解决方案1】:

    package documentation 包含误导性短语:“一个对象幻象可达,如果它既不是强、软或弱可达的,它已经被最终确定,并且一些幻象引用引用它。 ”

    “一些幻像引用”并没有强调引用对象本身必须是可访问的这一事实,这在 通知 部分中有说明:

    已注册的引用对象与其队列之间的关系是片面的。也就是说,队列不会跟踪向其注册的引用。如果已注册的引用本身变得不可访问,则它永远不会被加入队列。

    由于幻象可达没有任何实际后果,除了可能导致幻象引用排队之外,这才是最重要的。

    请注意,软可达性和弱可达性的定义更好:

    • 如果一个对象不是强可达但可以通过遍历软引用到达,那么它是软可达
    • 一个对象弱可达,如果它既不是强可达也不是软可达,但可以通过遍历弱引用来达到。 …

    这里强调的是对象必须是通过引用对象可达的,这意味着引用对象也必须是可达的。

    以类似方式定义幻象可达性的问题在于,幻象可达性对象实际上是不可达的,因为PhantomReference 覆盖get() 方法总是返回null,因此应用程序无法访问所指对象因此,不会遍历任何幻像可达对象。

    也许,一个更好的定义应该是

    • 一个对象是幻影可达,如果它既不是强、软或弱可达的,它已经被终结,但可以通过遍历一个垃圾收集器到达幻影参考。

    使用该定义,很明显,您的带有自引用的示例将不起作用,就像它不适用于 WeakReferenceSoftReference。请注意,当您的类没有专用的 finalize() 方法时,使用 WeakReferencePhantomReference 之间没有实际区别。

    似乎即使是 API 设计者也没有完全理解其中的含义,因为 specification prior to Java 9 包含规则:

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

    它公然忽略了幻像可达对象不可访问的点,至少从应用程序的角度来看,并从垃圾收集器的角度保持它幻像可达没有实际的好处。不让对象再次活跃,是最终确定的最大区别。但请注意,在这个地方,文档承认如果幻像引用本身变得不可访问,则引用对象将停止幻像可访问

    从 Java 9 开始,幻像引用在入队时会自动清除,因此对象从 幻像可达不可达 的转换的唯一实际含义是幻像引用被排队。这要求引用对象是可访问的。

    【讨论】:

      【解决方案2】:

      我认为你正在做的事情不会奏效。

      对象幻象可达的先决条件是:

      • 对象不是强可达、软可达或弱可达,
      • 对象已经完成,并且
      • 该对象可以从 GC 根通过至少一个带有幻像引用的路径访问。

      在您的情况下,前两个先决条件得到满足,但第三个不满足。如果我们假设this 不可访问,这意味着this.thisReference 也不可访问。这意味着 Foo 实例的 PhantomReference 将没有资格入队。

      (但是,它是“安全的”,因为它不会引发异常或使 JVM 崩溃,或有任何其他不良副作用。)

      【讨论】:

      • package doc 只说“一些幻像引用引用它”,但没有提到这个引用也必须是可访问的。尽管这可能只是不好的措辞,因为他们无法使用“可以到达”,因为 PhantomReferences 的 get 总是返回 null。或者你的最后一点是基于别的什么?
      • 嗯......我从不同的来源。我需要检查一下。
      • @Marcono1234 该声明仅告诉您有关幻象可达的正式标准。但是你想知道,它是否会入队,关于这一点,包文档很清楚:“注册的引用对象与其队列之间的关系是片面的。也就是说,队列不会跟踪向其注册的引用。如果已注册的引用本身变得无法访问,那么它将永远不会入队”。这背后有一个实际的考虑。如果GC遍历没有遇到Reference,则无法入队。
      • @Holger,您介意将其添加为单独的答案吗?斯蒂芬的这个答案更多地关注PhantomReference而不是排队。尽管您写的内容意味着它将取决于对象的字段或对象本身是否首先被认为是幻像可访问的,但这似乎没有定义。
      【解决方案3】:

      不,存储对this 的引用将不起作用:

      import java.lang.ref.PhantomReference;
      import java.lang.ref.ReferenceQueue;
      
      public class PhantomReferenceTest {
          private final PhantomReference<Object> thisReference;
      
          public PhantomReferenceTest(ReferenceQueue<Object> queue) {
              thisReference = new PhantomReference<>(this, queue);
          }
      
          public static void main(String[] args) throws InterruptedException {
              test(false);
              System.out.println("\nVerify that reference is enqueued if gc of thisReference is prevented:");
              test(true);
          }
      
          private static void test(boolean keepRefToRef) throws InterruptedException {
              ReferenceQueue<Object> queue = new ReferenceQueue<>();
              ReferenceQueue<Object> verifyQueue = new ReferenceQueue<>();
      
              PhantomReference<?> refToRef = null;
              PhantomReferenceTest obj = new PhantomReferenceTest(queue);
              PhantomReference<?> verifyReference = new PhantomReference<>(obj, verifyQueue);
      
              if (keepRefToRef) {
                  // Verify that reference is enqueued if it is kept alive
                  refToRef = obj.thisReference;
              }
      
              obj = null;
      
              System.gc();
              verifyQueue.remove();
              System.out.println("Object was collected");
              System.out.println("thisReference was enqueued: " + (queue.poll() != null));
      
              // Pretend to use refToRef to make sure gc cannot collect it
              if (refToRef != null) {
                  refToRef.get();
              }
          }
      }
      

      即使它会起作用,似乎也不能保证它在未来也会继续起作用。甚至不能保证一个字段在封闭类之后被认为是虚拟可达的。在 Java 12.0.1 中则相反:

      import java.lang.ref.PhantomReference;
      import java.lang.ref.ReferenceQueue;
      
      public class EnqueuingOrder {
          private static class RefToField extends PhantomReference<Object> {
              public RefToField(Object obj, ReferenceQueue<Object> queue) {
                  super(obj, queue);
              }
          }
      
          private final Object field;
      
          public EnqueuingOrder() {
              field = new Object();
          }
      
          public static void main(String[] args) throws InterruptedException {
              ReferenceQueue<Object> queue = new ReferenceQueue<>();
      
              EnqueuingOrder obj = new EnqueuingOrder();
              PhantomReference<?> refToObj = new PhantomReference<>(obj, queue);
              PhantomReference<?> refToField = new RefToField(obj.field, queue);
              obj = null;
      
              System.gc();
      
              System.out.println("First: " + queue.remove());
              System.out.println("Second: " + queue.remove());
          }
      }
      

      【讨论】:

      • 规范一致使用短语“在同一时间或稍后某个时间它将入队……”,这表明队列中的顺序是未指定的。但是,对于如此简单的设置,很容易说出会发生什么。两者是一起收集的。但是由于垃圾收集器在遍历过程中遇到引用时必须对其进行处理,因此顺序取决于它们在堆栈帧中的出现。所以只需交换这两行,PhantomReference&lt;?&gt; refToObj = …PhantomReference&lt;?&gt; refToField = … 将交换它们在队列中的顺序。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-24
      • 1970-01-01
      • 2014-06-15
      • 1970-01-01
      • 2013-01-23
      • 1970-01-01
      相关资源
      最近更新 更多