I know the different kinds of references, and reachability, and how the garbage collector works in general - I have also used the different types of Reference
s in some of my personal projects. However, there is one design choice I don't understand: Why the get
method of PhantomReference
s always returns null
.
PhantomReference
s are mostly (only?) used for cleanup actions after the object has "died". So, the usual answer to my question is "to ensure that the object can't get (strongly) reachable again after the cleanup has started" - or, as the documentation of PhantomReference puts it:
In order to ensure that a reclaimable object remains so, the referent of a phantom reference may not be retrieved: The get method of a phantom reference always returns null.
But as far as I understand, "ensuring that a reclaimable object remains so" doesn't imply that get()
must always return null
: As long as a phantom reference isn't cleared, its referent isn't reclaimable and also won't be enqueued, and thus the cleanup action won't have been started - meaning that get()
could "safely" give access to the referent. It only becomes reclaimable if the reference is cleared, so get()
would from then on return null
either way. There's also no possibility of race conditions (example: two PhantomRefs to one object; one gets cleared and enqueued, then the object is made accessible again through the other's get
method), because the documentation states that all clearing happens atomically (emphasis mine):
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.
Because the question came up in comments: I'm asking this not because I have a concrete problem for which I think this would be the solution. Instead, I need to describe the various Reference
types in my Master's thesis, where they are relevant, and I want to describe them correctly - including the reasoning behind the design choices.
However, I can still think of a concrete situation which would explicitly not be covered by using a WeakReference
:
Suppose you were writing a library which provided some service to its users. Let's assume the library wants to ensure that only one instance of this service exists at a time - that it is a "singleton", so to speak - so let's call the class PublicSingletonService
. However, let's assume the service needs many resources to exist - so, the library wants to throw it away once it's not needed anymore, and recreate it once it's needed again. I think this is a reasonable scenario.
So, the library holds a WeakReference
to its singleton, and uses a ReferenceQueue
to detect if the singleton is not needed anymore and to clean up. When the user requests the service, the library calls get()
on the WeakReference
, and if the result is non-null
, returns that, and if it is null
, it (re-)creates the service.
But now, the application using that library does the following:
System.gc()
, such that the GC clears the weak ref and starts the holder's finalizationThe library could partially guard against this by keeping a PhantomReference
in addition to the WeakReference
, and only cleaning up or creating a second instance if the PhantomReference
is cleared. However, this has a problem: If the WeakReference
is cleared, but the PhantomReference
is not, and the application asks the library to give it an instance of the service, the library can neither access the old instance nor create a new one.
If PhantomReference.get()
did not always return null
, this would be solvable: just let the library use a PhantomReference
instead of a WeakReference
, and use PhantomReference.get()
.
Here is an example program demonstrating this:
package weirdjava;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.Semaphore;
import weirdjava.StronglyReachableAfterWeakReferenceCleared.Library.PublicSingletonService;
public class StronglyReachableAfterWeakReferenceCleared
{
public static void main(String[] args) throws InterruptedException
{
Application.run();
}
public static class Application
{
public static void run() throws InterruptedException
{
// Application code initializes the library
Library library = new Library();
// "Library" creates the singleton of the public service and tries to detect once it is _definitely_ unreachable.
PublicSingletonService singleton = library.getSingleton();
// The application code then does weird stuff with the singleton (maliciously, bad programming, or convoluted interactions):
// make holder
ApplicationHolder holder = new ApplicationHolder(singleton);
// clear the last remaining strong ref to the singleton
singleton = null;
// make sure the libraries' weakref is actually cleared - simulates the application using much memory.
System.gc();
// The "library" tries to detect whether the object was cleared, and clean up if it is.
// In this example, we made sure that it is.
library.doCleanupIfNecessary();
// The application asks the library for the singleton again - because it has been cleared, the library recreates the singleton.
PublicSingletonService secondSingleton = library.getSingleton();
// Now, the application magically resurrects the first singleton
PublicSingletonService resurrectedSingleton = holder.getReferent();
if(resurrectedSingleton == null)
throw new IllegalStateException("example failed");
System.out.println("Resurrection successful! resurrected: " + resurrectedSingleton);
// At this point, the weak references to the object have been cleared,
// but the object is still strongly reachable -
// and that without the libraries' class having any finalizer.
System.out.println("Application now has two singletons: " + resurrectedSingleton + ", " + secondSingleton);
}
private static class ApplicationHolder
{
private final Semaphore referrerFinalizationComplete;
private FinalizableReferrer referrerAfterFinalization;
public ApplicationHolder(PublicSingletonService referent)
{
referrerFinalizationComplete = new Semaphore(0);
// explicitly don't keep a reference to this, to let it be able to be reclaimed
new FinalizableReferrer(this, referent);
}
public PublicSingletonService getReferent() throws InterruptedException
{
referrerFinalizationComplete.acquire();
return referrerAfterFinalization.getReferent();
}
private static class FinalizableReferrer
{
private final ApplicationHolder referrer;
private final PublicSingletonService referent;
public FinalizableReferrer(ApplicationHolder referrer, PublicSingletonService referent)
{
this.referrer = referrer;
this.referent = referent;
}
@Override
protected void finalize()
{
System.out.println("Finalizer making referent reachable again");
referrer.referrerAfterFinalization = this;
referrer.referrerFinalizationComplete.release();
}
public PublicSingletonService getReferent()
{
return referent;
}
}
}
}
public static class Library
{
// just for demonstration; in reality, this would have to be thread-safe and whatnot
private int instanceCounter = 0;
private final ReferenceQueue<PublicSingletonService> refqueue;
private WeakReference<PublicSingletonService> publicSingletonObjectRef;
public Library()
{
this.refqueue = new ReferenceQueue<>();
}
public void doCleanupIfNecessary() throws InterruptedException
{
// In practice, this would be poll(), not remove() - however, in this example,
// even though we called System.gc() the enqueueing doesn't happen fast enough.
Reference<? extends PublicSingletonService> poll = refqueue.remove();
if(poll != null)
{
System.out.println("Reference to referent was cleared; weakRef.get() is " + publicSingletonObjectRef.get()); // is null
// The library cleans up what it needs to clean up
System.out.println("Library \"cleans up\"");
} else
// In practice, the library would do nothing here.
throw new IllegalStateException("example failed");
}
public PublicSingletonService getSingleton()
{
if(publicSingletonObjectRef != null)
{
// This is the call to get() which would not be possible with a phantom reference
PublicSingletonService previousInstance = publicSingletonObjectRef.get();
if(previousInstance != null)
return previousInstance;
}
PublicSingletonService publicSingletonObjectInstance = new PublicSingletonService("Instance #" + instanceCounter ++);
publicSingletonObjectRef = new WeakReference<>(publicSingletonObjectInstance, refqueue);
return publicSingletonObjectInstance;
}
public static class PublicSingletonService
{
private final String value;
public PublicSingletonService(String value)
{
this.value = value;
System.out.println("Creating singleton object: " + value);
}
@Override
public String toString()
{
return value;
}
}
}
}
You are referring to the Java 20 API. But in this case, it’s useful to know the history.
Have a look at Java 8’s PhantomReference
documentation from 2022
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.
Don’t ask why keeping an object phantom reachable ever was a goal. That has been asked before and never got a satisfying answer. That’s part of the reason why this rule has been removed in Java 9 and as it seems, also in the latest Java 8 updates.
But when an enqueued PhantomReference
is not cleared, it’s obviously needed to prevent the retrieval via the get()
method, to prevent resurrection of reclaimable objects. Granted, deep reflection still worked back in the days, which is another indicator that this part of the API wasn’t well thought.
The current state of affairs is that phantom reference are cleared when enqueued and hence, nothing prevents the reclamation of the object while the post-mortem cleanup is running.
So, the original problem is gone. Allowing get()
to return the referent would not affect the way, garbage collection works. It would become possible to retrieve the object while finalization is in progress, but this would only be a semantic problem, but not affect the logic of phantom references, as they are only enqueued when the object became unreachable (again) after finalization completed.
However, there is no reason to change the contract. Getting the object back is not necessary for the purpose of phantom references. That’s what weak references are for and since finalization has been marked “deprecated, for removal”, any problems related to finalization will vanish anyway.
It’s true that while finalization still exists, the scenario of resurrection exist when using weak references. However, if someone tries that hard to break your program, nothing will stop them anyway. Don’t let such people contribute to your code base. Java’s language mechanisms are not real security measures. They make it harder to write bad code, but can’t prevent people from deliberately breaking things.
That said, the entire idea to use garbage collection to release non-memory resources in the way you described, is flawed. There is no guaranty about when or whether at all the object will get collected, hence in your scenario this cleanup feature is not worth the development effort. But in the worst case, it will break horribly.