javagarbage-collectionphantom-reference

Inner fields of a class are not collected by GC via Phantom References


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?


Solution

  • 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.