I am having problems with Phantom References when referents are the fields inside the class. When class objects are set to null, fields are not collected automatically by GC
Controller.java
public class Controller {
public static void main( String[] args ) throws InterruptedException
{
Collector test = new Collector();
test.startThread();
Reffered strong = new Reffered();
strong.register();
strong = null; //It doesn't work
//strong.next =null; //It works
test.collect();
Collector.m_stopped = true;
System.out.println("Done");
}
}
Collector.java: I am having a Collector that registers an object to reference queue and prints it when it is collected.
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
public class Collector {
private static Thread m_collector;
public static boolean m_stopped = false;
private static final ReferenceQueue refque = new ReferenceQueue();
Map<Reference,String> cleanUpMap = new HashMap<Reference,String>();
PhantomReference<Reffered> pref;
public void startThread() {
m_collector = new Thread() {
public void run() {
while (!m_stopped) {
try {
Reference ref = refque.remove(1000);
System.out.println(" Timeout ");
if (null != ref) {
System.out.println(" ref not null ");
}
} catch (Exception ex) {
break;
}
}
}
};
m_collector.setDaemon(true);
m_collector.start();
}
public void register(Test obj) {
System.out.println("Creating phantom references");
//Referred strong = new Referred();
pref = new PhantomReference(obj, refque);
cleanUpMap.put(pref, "Free up resources");
}
public static void collect() throws InterruptedException {
System.out.println("GC called");
System.gc();
System.out.println("Sleeping");
Thread.sleep(5000);
}
}
Reffered.java
public class Reffered {
int i;
public Collector test;
public Test next;
Reffered () {
test= new Collector();
next = new Test();
}
void register() {
test.register(next);
}
}
Test is a empty class. I can see that "next" field in Refferred class is not collected when Reffered object is set to null. In other words, when "strong" is set to null, "next" is not collected. I assumed that "next" will be automatically collected by GC because "next" is no more referenced when "strong" is set to null. However, when "strong.next" is set to null, "next" is collected as we think. Why is "next" not collected automatically when strong is set to null?
You have a very confusing code structure.
At the beginning of your code, you have the statements
Collector test = new Collector();
test.startThread();
so you are creating an instance of Collector
that the background thread will have a reference to. That thread isn’t even touching that reference, but since it is an anonymous inner class, it will hold a reference to its outer instance.
Within Reffered
you have a field of type Collector
that is initialized with new Collector()
in the constructor, in other words, you are creating another instance of Collector
. This is the instance on which you invoke register
.
So all artifacts created by register
, the PhantomReference
held in pref
and the HashMap
held in cleanUpMap
, which has also a reference to the PhantomReference
, are only referenced by the instance of Collector
referenced by Reffered
. If the Reffered
instance becomes unreachable, all these artifacts become unreachable too and nothing will be registered at the queue.
This is the place to recall the java.lang.ref
package documentation:
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. It is the responsibility of the program using reference objects to ensure that the objects remain reachable for as long as the program is interested in their referents.
There are some ways to illustrate the issue with your program.
Instead of doing either, strong = null;
or strong.next = null;
, you may do both:
strong.next = null;
strong = null;
here, it doesn’t matter that next
has been nulled out, this variable is unreachable anyway, once strong = null
has been executed. After that, the PhantomReference
that was only reachable through the Reffered
instance has become unreachable itself and no “ref not null” message will be printed.
Alternatively, you may change that code part to
strong.next = null;
strong.test = null;
which will also make the PhantomReference
unreachable, thus never enqueued.
But if you change it to
Object o = strong.test;
strong = null;
the message “ref not null” will be printed as o
holds an indirect reference to the PhantomReference
. It must be emphasized that this is not guaranteed behavior, Java is allowed to eliminate the effect of unused local variables. But it is sufficiently reproducible with the current HotSpot implementation to demonstrate the point.
The bottom line is, the Test
instance has been always collected as expected. It’s just that in some cases, more has been collected than you were aware of, including the PhantomReference
itself, so no notification happened.
As a last remark, a variable like public static boolean m_stopped
that you share between two threads must be declared volatile
to ensure that a thread will notice modifications made by another thread. It happens to work here without, because the JVM’s optimizer did not do much work for such a short running program and architectures like x68 synchronize caches. But it’s unreliable.