3

I have a shared resource for which I would like to know how many other objects are still using this resource. To do this I would like to use PhantomReferences.

Since ReferenceQueues do not keep track of the references registered for them (source, section "Notification"), my idea was to store the reference as field of the tracked object:

class Foo {
    private PhantomReference<Foo> thisReference;

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

Is this safe, based on Java 9(+) behavior of PhantomReferences, or is it possible that an instance is garbage collected without the reference being added to the queue?

The documentation says:

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.

But it does not mention whether the garbage collection can happen before the references are enqueued.

Marcono1234
  • 5,856
  • 1
  • 25
  • 43

3 Answers3

3

I don't think that what you are doing will work.

The prerequisites for an object to be phantom reachable are:

  • the object is not strongly reachable, softly reachable or weakly reachable,
  • the object has been finalized, and
  • the the object is reachable from a GC root via at least one path with a phantom reference.

In your case, the first two prerequisites are satisfied, but the third is not. If we assume that this is not reachable, that means that this.thisReference isn't reachable either. That implies that a Foo instance's PhantomReference will not be eligible to be enqueued.

(However, it is "safe" in the sense that this won't throw an exception or crash the JVM, or have any other undesirable side-effects.)

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • The [package doc](https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/lang/ref/package-summary.html#reachability) only says "and some phantom reference refers to it", but does not mention that this reference has to be reachable as well. Though maybe this is just bad wording because they could not use "can be reached" since PhantomReferences' `get` always return `null`. Or is your last point based on something else? – Marcono1234 Jun 02 '19 at 13:22
  • Hmmm ... I was going from a different source. I will need to check this. – Stephen C Jun 03 '19 at 01:02
  • 1
    @Marcono1234 that statement only tells you something about the formal criteria for being *phantom reachable*. But you want to know, whether it will get enqueued, and regarding that, the package documentation is clear: “*The relationship between a registered reference object and its queue is one-sided. That is, a queue does not keep track of the references that are registered with it. If a registered reference becomes unreachable itself, then it will never be enqueued*”. There is a practical consideration behind it. If the GC traversal does not encounter a `Reference`, it can not enqueue it. – Holger Jun 20 '19 at 14:20
  • @Holger, would you mind adding this as separate answer? This answer by Stephen focuses more on the `PhantomReference` than on enqueuing. Though what you wrote means it would then be dependent on whether a field of an object or the object itself is considered phantom-reachable first, however this appears to be not defined. – Marcono1234 Jun 20 '19 at 16:27
3

The package documentation contains the misleading phrase: “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.”

That “some phantom reference” does not emphasize the fact that the reference object itself must be reachable, which is stated in the Notification section:

The relationship between a registered reference object and its queue is one-sided. That is, a queue does not keep track of the references that are registered with it. If a registered reference becomes unreachable itself, then it will never be enqueued.

Since being phantom reachable has no practical consequences, other than potentially causing the enqueuing of phantom references, this is all that matters.

Note that the definitions of soft and weak reachability are better:

  • An object is softly reachable if it is not strongly reachable but can be reached by traversing a soft reference.
  • An object is weakly reachable if it is neither strongly nor softly reachable but can be reached by traversing a weak reference. …

Here, it is emphasized that the object must be reachable through the reference objects, which implies that the reference objects must be reachable too.

The problem with defining phantom reachability in a similar way, is, that phantom reachable objects are actually unreachable, as the PhantomReference overrides the get() method to always return null, so an application can not reach the referent and hence, not traverse any phantom reachable objects.

Perhaps, a better definition would have been

  • An object is phantom reachable if it is neither strongly, softly, nor weakly reachable, it has been finalized, but can be reached by the garbage collector by traversing a phantom reference.

With that definition, it would be pretty clear that your example with a self-reference would not work, just like it wouldn’t work with a WeakReference or SoftReference. Note that when your class has no dedicated finalize() method, there is no practical difference between using a WeakReference or a PhantomReference.

It seems that even the API designers did not understand the implications completely, as the specification prior to Java 9 contained the rule:

Unlike soft and weak references, phantom references are not automatically cleared by the garbage collector as they are enqueued. An object that is reachable via phantom references will remain so until all such references are cleared or themselves become unreachable.

which blatantly ignores the point that a phantom reachable object isn’t reachable, at least from an application point of view and keeping it phantom reachable from a garbage collector’s view has no practical benefit. Not making the object live again, is the very difference to finalization. But note that at this place, the documentation acknowledged that if a phantom reference itself becomes unreachable, the referent stops being phantom reachable.

Starting with Java 9, phantom references are automatically cleared when enqueued, so the only practical implication of an object’s transition from phantom reachable to unreachable, is the phantom reference being enqueued. Which requires the reference object to be reachable.

Holger
  • 285,553
  • 42
  • 434
  • 765
0

No, storing a reference to this will not work:

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();
        }
    }
}

Even if it would work there appears to be no guarantees that it would also continue to work in the future. It is not even guaranteed that a field is considered phantom-reachable after the enclosing class. In Java 12.0.1 the opposite is the case:

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());
    }
}
Marcono1234
  • 5,856
  • 1
  • 25
  • 43
  • 1
    The specification consistently uses the phrase “at the same time or at some later time it will enqueue…”, which indicates that the order in the queue is unspecified. However, for such a simple setup, it’s easy to say what happens. Both are collected together. But since the garbage collector has to process the references when encountering them during the traversal, the order depends on their occurrences in the stack frame. So just swapping the two lines, `PhantomReference> refToObj = …` and `PhantomReference> refToField = …` will swap their order in the queue. – Holger Jun 20 '19 at 17:30