Hi! In today's discussion, we'll talk in detail about "phantom references" (PhantomReference) in Java. What kind of references are these? Why are they called "phantom references"? How are they used?
As you'll recall, Java has 4 kinds of references:
StrongReference (ordinary references that we create when creating an object):
Cat cat = new Cat()
In this example, cat is a strong reference.
SoftReference (soft reference). We had a lesson about such references.
WeakReference (weak reference). There was also a lesson about them here.
PhantomReference (phantom reference).
get() — returns the referenced object;
- clear() — removes the reference to the object.
finalize()
method has been overridden for the object, it is called. Or maybe it's not called — it all depends only whether you're lucky. You probably remember that finalize()
is fickle :)
In the garbage collector's second pass, the object is deleted and memory is freed.
The garbage collector's unpredictable behavior creates a number of problems for us.
We don't know exactly when the garbage collector will start running. We don't know whether the finalize()
method will be called. Plus, a strong reference to an object can be created while its finalize()
method is being executed, in which case the object will not be deleted at all. For programs that make heavy demands on available memory, this can easily lead to an OutOfMemoryError
.
All this pushes us to use phantom references.
The fact is that this changes the garbage collector's behavior. If the object has only phantom references, then:
its finalize() method is called (if it is overridden)
if nothing changes once the finalize() method is finished and the object can still be deleted, then the phantom reference to the object is placed in a special queue: ReferenceQueue.
public class TestClass {
private StringBuffer data;
public TestClass() {
this.data = new StringBuffer();
for (long i = 0; i < 50000000; i++) {
this.data.append('x');
}
}
@Override
protected void finalize() {
System.out.println("The finalize method has been called on the TestClass object");
}
}
When we create objects, we'll intentionally give them a hefty "load" (by adding 50 million "x" characters to each object) in order to take up more memory.
In addition, we override the finalize() method to see that it is run.
Next, we need a class that will inherit from PhantomReference. Why do we need such a class?
It's all straightforward. This will allow us to add additional logic to the clear() method in order to verify that the phantom reference really is cleared (which means the object has been deleted).
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class MyPhantomReference<TestClass> extends PhantomReference<TestClass> {
public MyPhantomReference(TestClass obj, ReferenceQueue<TestClass> queue) {
super(obj, queue);
Thread thread = new QueueReadingThread<TestClass>(queue);
thread.start();
}
public void cleanup() {
System.out.println("Cleaning up a phantom reference! Removing an object from memory!");
clear();
}
}
Next, we need a separate thread that will wait for the garbage collector to do its job, and phantom links will appear in our ReferenceQueue. As soon as such a reference ends up in the queue, the cleanup() method is called on it:
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
public class QueueReadingThread<TestClass> extends Thread {
private ReferenceQueue<TestClass> referenceQueue;
public QueueReadingThread(ReferenceQueue<TestClass> referenceQueue) {
this.referenceQueue = referenceQueue;
}
@Override
public void run() {
System.out.println("The thread monitoring the queue has started!");
Reference ref = null;
// Wait until the references appear in the queue
while ((ref = referenceQueue.poll()) == null) {
try {
Thread.sleep(50);
}
catch (InterruptedException e) {
throw new RuntimeException("Thread " + getName() + " was interrupted!");
}
}
// As soon as a phantom reference appears in the queue, clean it up
((MyPhantomReference) ref).cleanup();
}
}
And finally, we need the main() method, which we will put it in a separate Main class.
In that method, we'll create a TestClass object, a phantom reference to it, and a queue for phantom references. After that, we'll call the garbage collector and see what happens :)
import java.lang.ref.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(10000);
ReferenceQueue<TestClass> queue = new ReferenceQueue<>();
Reference ref = new MyPhantomReference<>(new TestClass(), queue);
System.out.println("ref = " + ref);
Thread.sleep(5000);
System.out.println("Collecting garbage!");
System.gc();
Thread.sleep(300);
System.out.println("ref = " + ref);
Thread.sleep(5000);
System.out.println("Collecting garbage!");
System.gc();
}
}
Console output:
ref = MyPhantomReference@4554617c
The thread monitoring the queue has started!
Collecting garbage!
The finalize method has been called on the TestClass object
ref = MyPhantomReference@4554617c
Collecting garbage!
Cleaning up a phantom reference!
Removing an object from memory!
What do we see here?
Everything happened as we planned!
Our object's finalize() method is overridden and it was called while the garbage collector was running.
Next, the phantom reference was put into the ReferenceQueue. While there, its clear() method was called (within which we called cleanup() in order to output to the console).
Finally, the object was deleted from memory.
Now you see exactly how this works :)
Of course, you don't need to memorize all of the theory about phantom references. But it would be good if you remember at least the main points.
First, these are the weakest references of all.
They come into play only when no other references to the object are left.
The list of references that we gave above is sorted in descending order from strongest to weakest:
StrongReference -> SoftReference -> WeakReference -> PhantomReference
A phantom reference enters the battle only when there are no strong, soft, or weak references to our object :)
Second, the get() method always returns null for a phantom reference.
Here is a simple example where we create three different types of references for three different types of cars:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
Sedan sedan = new Sedan();
HybridAuto hybrid = new HybridAuto();
F1Car f1car = new F1Car();
SoftReference<Sedan> softReference = new SoftReference<>(sedan);
System.out.println(softReference.get());
WeakReference<HybridAuto> weakReference = new WeakReference<>(hybrid);
System.out.println(weakReference.get());
ReferenceQueue<F1Car> referenceQueue = new ReferenceQueue<>();
PhantomReference<F1Car> phantomReference = new PhantomReference<>(f1car, referenceQueue);
System.out.println(phantomReference.get());
}
}
Console output:
Sedan@4554617c
HybridAuto@74a14482
null
The get() method returned entirely ordinary objects for the soft and weak references, but it returned null for the phantom reference.
Third, phantom references are mainly used in complicated procedures for deleting objects from memory.
That's it! :) That concludes our lesson today. But you can't go far on theory alone, so it's time to return to solving tasks! :)
GO TO FULL VERSION