Java PhantomReference vs finalize()
Asked Answered
I

1

5

I've been reading this article about PhantomReference https://www.baeldung.com/java-phantom-reference and simplified sample code found there:

public static void main(String[] args) throws InterruptedException {
    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    Object object = new Object();
    PhantomReference<Object> phantomReference = new PhantomReference<>(object, referenceQueue);
    object = null;
    System.gc();
    Thread.sleep(1_000);
    System.out.println("isEnqueued() after GC: " + phantomReference.isEnqueued());
    Reference reference = referenceQueue.poll();
    if(reference != null) {
        System.out.println("isEnqueued() after poll(): " + phantomReference.isEnqueued());
    }
}

Here is output:

isEnqueued() after GC: true
isEnqueued() after poll(): false

So everything is working as expected, strong reference to object is set to null which is detected by GC and phantom reference is added to the queue.

Now in that article they say: "The Garbage Collector adds a phantom reference to a reference queue after the finalize method of its referent is executed. It implies that the instance is still in the memory."

So I wanted to make a test and override finalize method like:

Object object = new Object() {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize()");
    }
};

But then output is different, phantom reference is not added to the queue anymore:

finalize()
isEnqueued() after GC: false

Could somebody explain why after this change output is different and how to change this code so phantom reference would be added to queue?

I've been testing this on JDK 8 and 11, same results on both platforms.

Illuminator answered 17/12, 2018 at 19:53 Comment(0)
S
12

The statement “The Garbage Collector adds a phantom reference to a reference queue after the finalize method of its referent is executed.” is a bit sloppy at best.

You should refer to the specification:

If the garbage collector determines at a certain point in time that the referent of a phantom reference is phantom reachable, then at that time or at some later time it will enqueue the reference.

Whereas the linked definition of “phantom reachable” states:

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.

So an object is phantom reachable “after the finalize method of its referent is executed” if only referenced by phantom references and hence will be enqueued after that but not immediately. Since an object is strongly reachable during the execution of its finalize() method, it takes at least one additional garbage collection cycle to detect that it became phantom reachable. Then, “at that time or at some later time” it will get enqueued.

If you change the program to

ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
Object object = new Object() {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize()");
    }
};
PhantomReference<Object> phantomReference=new PhantomReference<>(object, referenceQueue);
object = null;
System.gc();
Thread.sleep(1_000);
System.gc();
Thread.sleep(1_000);
System.out.println("isEnqueued() after GC: " + phantomReference.isEnqueued());
Reference reference = referenceQueue.poll();
if(reference != null) {
    System.out.println("isEnqueued() after poll(): " + phantomReference.isEnqueued());
}

You will most likely see the desired output, but it has to be emphasized that there are no guarantees that the garbage collector will actually run when you call System.gc() or that it completes in a particular amount of time when it runs or that it will find all unreachable objects within a particular cycle. Further, the enqueuing happens asynchronously after the gc cycle so even by the time the garbage collector completed and detected a special reachability state, an additional amount of time may pass before the reference gets enqueued.


Note that the sentence “It implies that the instance is still in the memory.” also doesn’t get it right, but in this case, it’s based on a misunderstanding that was even on the Java core developer’s side.

When the API was created, a sentence was added to the specification which you will find even in the Java 8 version:

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.

This may lead to the naïve assumption that the object still has to be in memory, but The Java® Language Specification states:

Optimizing transformations of a program can be designed that reduce the number of objects that are reachable to be less than those which would naively be considered reachable.

Simply said, the memory of objects may get reclaimed earlier, if the behavior of the program does not change. This applies especially to scenarios, where the application can not use the object at all, like with the phantom reference. The behavior of the program wouldn’t change if the object is not in memory anymore, so you can not assume that it actually is.

This leads to the question, why the rule that phantom references are not cleared was added to the spec at all. As discussed in this answer, that question was brought up and could not be answered at all. Consequently, this rule has been removed in Java 9 and phantom reference are cleared when enqueued, like weak and soft reference. That’s an even stronger reason not to assume that the object is still in memory, as now even non-optimizing environments can reclaim the object’s memory at this point.

Seamanship answered 18/12, 2018 at 8:27 Comment(3)
Thanks for very detailed explanation! Can I ask about one more clarification about this sentence: "phantom references are not automatically cleared by the garbage collector as they are enqueued". So it means that strong reference to referent object is not null(I'm looking at source code of java.lang.ref.Reference class) in Java 8? And since Java 9 it's null after phantom reference has been added to the queue? I guess that's just technical difference as that strong reference is not accessible via get() method, just invoking clear() method has some difference when it comes to Java 8 vs 9?Illuminator
Yes, it means that the referent field stays non-null whereas the get() method forcibly always returns null, The combination of these two properties ensures that “an object that is reachable via phantom references will remain so” though it is unclear why that should be an actual goal. For the application, it makes no difference, as the referent is not accessible. It doesn’t even change the behavior of clear(), as that method simply sets the referent field to null, whether it was null before or not. Only the gc does not need an additional cycle anymore to reclaim the memory.Seamanship
actually Shenandoah at least, would still keep the referent alive for PhantomReferences that are themselves reachable; it stopped doing that only since java-9. I can not vote twice, but a very good answer.Arlo

© 2022 - 2024 — McMap. All rights reserved.