5

I have a PR, an object O to which the PR points, and an RQ set up for PR. I have a thread which keeps on polling the RQ, and at the first reference it finds in RQ, the thread prints the time at which it found it, and exits.

Things work fine, but the moment O has a finalize (no matter how trivial), the thread no longer finds a reference in RQ and keeps running indefinitely.

Question: why does it happen this way? I'm using Sun JDK 1.6.

Here's the code:

good case

public class MyGCPhantom 
{   
    public static void main(String[] args) throws InterruptedException 
    {       
        GCPhantomObject p = new GCPhantomObject();
        ReferenceQueue phantomQueue = new ReferenceQueue();
        PhantomReference<GCPhantomObject> pr = new PhantomReference<GCPhantomObject>(p, phantomQueue);      
        new GCPhantomThread(phantomQueue, "Phantom").start();
        p = null;

        System.gc();
    }
}

class GCPhantomObject
{   
    @Override
    protected void finalize()
    {
        //System.out.println("GCPhantom finalized " + System.currentTimeMillis());      
    }
}

class GCPhantomThread extends Thread
{
    private ReferenceQueue referenceQueue;
    private String name;

    GCPhantomThread(ReferenceQueue referenceQueue, String name)
    {
        this.referenceQueue = referenceQueue;
        this.name = name;
    }

    @Override
    public void run()
    {
        while(referenceQueue.poll() == null);       
        System.out.println(name + " found at " + System.currentTimeMillis());
    }
}

bad case

Just uncomment the SOP in finalize() of GCPhantomObject.

shrini1000
  • 7,038
  • 12
  • 59
  • 99
  • Not calling super.finalize() might be part of the problem (although you don't do that in the good case either). – Walter Laan Oct 17 '12 at 11:24

4 Answers4

4

Your analysis is somewhat off. In both the good case and the bad case your object implements finalize. In the good case, it implements it trivially; in the bad case, non-trivially. Therefore the obvious problem is in the difference between a trivial and non-trivial implementation of finalize.

I see no reason why JVM would be forced by specification to enqueue your refs. You do a single GC run and then just keep waiting for something to happen. It is known that any nontrivial finalizer may resurrect the object, therefore there may be more GC cycles needed before it is enqueued. I suggest adding more GC calls.

Also note that your decision to use poll instead of remove is not advised. You should use a blocking call to prevent busy-polling.

For reference, these are the relevant definitions from the documentation:

If the garbage collector determines at a certain point in time that the referent of a phantom reference is phantom reachable, then at that time or at some later time it will enqueue the reference.


An object is phantom reachable if it is neither strongly, softly, nor weakly reachable, it has been finalized, and some phantom reference refers to it.


A finalized object has had its finalizer automatically invoked.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • Very well, but it still doesn't answer my question. Why does it happen the way it does? I see the same problem even if I replace that SOP with something as simple as 'int y = 0;' – shrini1000 Oct 17 '12 at 12:04
  • I first replaced poll() with remove(), didn't help. Then I replaced SOP with that 'int...', didn't help. When I made the finalize() trivial (empty body) it worked independent of if I used poll() or remove(). – shrini1000 Oct 17 '12 at 12:09
  • Hmm, worked with two gc calls without any delay between them. Thanks for this explanation. – shrini1000 Oct 17 '12 at 12:29
1

The Phantom Reference won't appear in the ReferenceQueue until after the object have been finalized. You are doing a busy loop so it is problematic. Note that finalization requires at least two gcs.

Shimi Bandiel
  • 5,773
  • 3
  • 40
  • 49
1

I just tried the code posted here on my system and it does not work even after two System.gc() calls. It does not terminate even if place System.gc() calls in the while loop of GCPhantomThread class here.

It seems to me that the issue here is that the object you are creating never gets placed in the ReferenceQueue because it is not even phantom-reachable when the GCPhantomThread is running. The PhantomReference to the object in the main() method falls out of scope and so when you have the GCPhantomThread running, the object is not even phantom-reachable. According to the docs, for the phantom reference to be enqueued, finalization and phantom-reachability are necessary.

It works when I pass the phantom reference to GCPhantomThread. On my machine this code always terminates:



    import java.lang.ref.PhantomReference;
    import java.lang.ref.ReferenceQueue;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;

    public class MyGCPhantom {
        public static void main(String[] args) throws InterruptedException {
            GCPhantomObject p = new GCPhantomObject();
            ReferenceQueue phantomQueue = new ReferenceQueue();
            PhantomReference pr = new PhantomReference(p, phantomQueue);
            new GCPhantomThread(pr, phantomQueue, "Phantom").start();
            p = null;
            pr = null;
            System.gc();
            System.out.println("main thread done ...");
        }
    }

    class GCPhantomObject {
        @Override
        protected void finalize() {
            System.out.println("GCPhantom finalized at " + System.nanoTime());
        }
    }

    class GCPhantomThread extends Thread {
        private ReferenceQueue referenceQueue;
        private String name;
        private PhantomReference pr;

        GCPhantomThread(PhantomReference pr, ReferenceQueue referenceQueue, String name) {
            this.referenceQueue = referenceQueue;
            this.name = name;
            this.pr = pr;
        }

        @Override
        public void run() {
            try {
                while (referenceQueue.remove(5000) == null) {
                    System.gc();
                }
                System.out.println(name + " found at " + System.nanoTime());
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

abhinav
  • 221
  • 1
  • 3
  • 13
  • The object in main() is only phantom reachable when GC is called, because the stack frame is not popped yet, so for GC, it sees only a PR to that object and hence collects it. The PR goes out of scope *after* main() returns, and not during the GC. In your case, did you use Sun JDK 6? I consistently get this behavior with it. – shrini1000 Oct 22 '12 at 07:48
0

Found an excellent article on Finalizer by Jack Shirazi. The question answers itself once we go through the article.

http://www.fasterj.com/articles/finalizer1.shtml

To explain it in a nutshell: in the case of a non trivial finalize() method, even though the object is 'collected' in the first GC run, it is not physically deleted at that time. That happens on the next GC run. That's why the PR object will appear on the queue during second GC.

shrini1000
  • 7,038
  • 12
  • 59
  • 99