Monitoring close() via References
In this story, we investigate an approach to monitor calls to close()
method from the AutoCloseable
interface in Java. The explored approach relies on weak and phantom references.
Read also: docs.oracle.com/en/java/javase/17/docs/api/
java.base/java/lang/ref/PhantomReference.html, and references therein.
A non-example
To warm up a bit in the topic of the text, let’s consider a very basic example of a monitored resource-handler:
When the Garbage Collector will try to finalize the object, the finalize
method is going to be called. In that method, a check of whether or not the object has been closed is performed.
Here the action is logging, but you could imagine different things. The crucial point is to make sure you don’t strongly reference your object again somewhere else, to prevent eventual garbage collection of it.
Our goal is about providing a similar behavior without rewriting finalization and making use of references instead.
The three kinds of references
(This section is non formal and aimed at refreshing memories, without providing a comprehensive guide on the topic; mostly because I’m not sure to get it all; feel free to bring some accuracy)
Strong reference
A strong reference is the most standard way of referencing an object. Basically, an object is strongly reachable when some variable holds the reference to the object. Typically:
var a = new A();
// the object A is strongly reachable,
// as some variable holds a ref to ita = null;
// the object is not strongly reachable
When an object is strongly reachable, it cannot be collected by the GC alone.
Finalization
When the GC detects an eligible object, it will call its finalize method. The object becomes finalized. This is guaranteed to happen only once.
If you store in the finalize method the object reference somewhere else (strongly), the finalization won’t occur anymore and the object won’t be eventually garbage collected.
Phantom reachable
An object is phantom reachable when it is not strongly reachable, and it is not held by a weak reference (or a soft one). It must also have been finalized and a PhantomReference
has to refer to it.
It is never possible for a PhantomReference
to leak the reference to the object: the get()
method always returns null:
var a = new A();
var phantomReference = new PhantomReference<>(a, queue);
// here, the object is still strongly reachable
assert phantomReference.get() == null;
assert a != null;a = null;
// we stop strongly referencing the object
// the object may, or not, be phantom reachable
// (depending on the finalization)assert phantomReference.get() == null;
The queue
parameter is a java.lang.ref.ReferenceQueue
. A reference queue can be used thread-safely to get phantom references that are phantom reachable. At that point, we know that no other reference can point to our object, and it has been finalized.
Weakly reachable
An object is weakly reachable if it is not strongly reachable and it some WeakReference
refers to it.
It is possible for a WeakReference
to get the object back via the get()
method, but whether or not the returned reference is null is nondeterministic. In fact, it is guaranteed that the get()
method will return the object reference when the object is still strongly reachable.
var a = new A();
var weakReference = new WeakReference<>(a);
// here, the object is still strongly reachable
assert weakReference.get() == a;a = null;
// we stop strongly referencing the object
// the object may, or not, be weakly reachableif ((a = weakReference.get()) != null) {
// the object is now strongly referenced again!
}// only when weakReference.get() == null,
// will the object be finalized.
Weak references are therefore strong enough to get the object back sometimes, but they are not strong enough to prevent garbage collection.
Implementing the monitor
Let’s go back to out monitoring problem.
What we would like is, in another monitoring thread, keep the state of some specific resource kind:
The resource-handler registers itself on our monitor object when it is initialized. After a close()
operation, the object alerts the monitor to get free.
The override of finalize()
is here for logging purpose only.
Monitor dynamic
The monitor should keep a state of whether or not an object has been closed. This suggests some map Object -> Boolean
.
The issue we could have by doing this, is that the monitor holds strong references to objects, which can be quite problematic, since the monitor won’t detect whether or not the object is garbage-collected: it won’t!
This remarks suggests rather the use of a map PhantomReference -> Boolean
together with a ReferenceQueue
. When the monitor will poll a phantom reachable reference from the queue, it can checkout the map and see whether or not the related AutoCloseable
has been closed.
If we do this, we are going to run in another problem: it is now impossible for our object to alert the monitor it has been closed. Indeed, PhantomReference
cannot be used to know which object they refer to, and their equality is performed by identity. This means that we cannot alter the map to push the closing-state.
The workaround we suggest is to maintain another map of WeakReference -> PhantomReference
, and play with the following collaboration:
When an AutoCloseable
object of class A
registers, it stores a pair (weakRef, phantomRef)
of references to itself, together with a pair (phantomRef, Boolean)
.
When the object finishes its close()
method, the free()
method is called. At that point, the object is still strongly reachable (it is in the method argument!). The weak reference is guaranteed to return the object reference, and from that knowledge we can remove the weak reference and get back the associated phantom reference.
The object is not weakly reachable and we hook the value Boolean = TRUE
in the map of close-states.
When the object will become finalized, the reference queue will eventually poll the phantom reference and the monitor will be able to check if the object has indeed been closed properly.
Stress test!
For the stress test, we are going to run this simple scenario:
The monitor runs in another thread. The object is created, then closed, and then the only strong reference to it is nullified.
We update GC-thread priority and we relax a bit. Then we run some stress test that allocate a ton of integers on my heap, to tickle a bit garbage collection. The object cannot coexist with the stress test (my computer is not strong enough).
Here is the result:
As you can see, object finalization occurs faster than the poll result of the queue. It rises a bit after the System.gc()
calls. The object is phantom reachable only after the first stress iteration, likely because the system really needed the memory at that point.
The monitor has detected successfully our object was closed.
Here is the same test, when we comment the a.close();
instruction:
The sequence of events hasn’t changed much. The difference is that our monitor successfully detected the object was not closed.
As a side note, if we forgot to free our object from the monitor, the monitor will think the object was never closed and warns in that direction. It is thus a pessimistic approach.
That’s all for me for today!
I hope you enjoyed reading and it gave you the push to play a bit with the reference feature.
Usages could be quite cool, like resource logging. Have you other ideas? Don’t hesitate to share them in comments :-)