Why won't my objects die?
Asked Answered
D

1

7

I'm trying to implement a mechanism that deletes cached files when the objects that hold them die, and decided to use PhantomReferences to get notified on garbage collection of an object. The problem is I keep experiencing weird behavior of the ReferenceQueue. When I change something in my code it suddenly doesn't fetch objects anymore. So I tried to make this example for testing, and ran into the same problem:

public class DeathNotificationObject {
    private static ReferenceQueue<DeathNotificationObject> 
            refQueue = new ReferenceQueue<DeathNotificationObject>();

    static {
        Thread deathThread = new Thread("Death notification") {
            @Override
            public void run() {
                try {
                    while (true) {
                        refQueue.remove();
                        System.out.println("I'm dying!");
                    }
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
        };
        deathThread.setDaemon(true);
        deathThread.start();
    }

    public DeathNotificationObject() {
        System.out.println("I'm born.");
        new PhantomReference<DeathNotificationObject>(this, refQueue);
    }

    public static void main(String[] args) {
        for (int i = 0 ; i < 10 ; i++) {
            new DeathNotificationObject();                  
        }
        try {
            System.gc();    
            Thread.sleep(3000); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The output is:

I'm born.
I'm born.
I'm born.
I'm born.
I'm born.
I'm born.
I'm born.
I'm born.
I'm born.
I'm born.

Needless to say, changing the sleep time, calling gc multiple times etc. didn't work.

UPDATE

As suggested, I called Reference.enqueue() of my reference, which solved the problem.

The weird thing, is that I have some code that works perfectly (just tested it), although it never calls enqueue. Is it possible that putting the Reference into a Map somehow magically enqueued the reference?

public class ElementCachedImage {
    private static Map<PhantomReference<ElementCachedImage>, File> 
            refMap = new HashMap<PhantomReference<ElementCachedImage>, File>();
    private static ReferenceQueue<ElementCachedImage> 
            refQue = new ReferenceQueue<ElementCachedImage>();

    static {
        Thread cleanUpThread = new Thread("Image Temporary Files cleanup") {
            @Override
            public void run() {
                try {
                    while (true) {
                        Reference<? extends ElementCachedImage> phanRef = 
                                refQue.remove();
                        File f = refMap.remove(phanRef);
                        Calendar c = Calendar.getInstance();
                        c.setTimeInMillis(f.lastModified());
                        _log.debug("Deleting unused file: " + f + " created at " + c.getTime());
                        f.delete();
                    }
                } catch (Throwable t) {
                    _log.error(t);
                }
            }
        };
        cleanUpThread.setDaemon(true);
        cleanUpThread.start();
    }

    ImageWrapper img = null;

    private static Logger _log = Logger.getLogger(ElementCachedImage.class);

    public boolean copyToFile(File dest) {
        try {
            FileUtils.copyFile(img.getFile(), dest);
        } catch (IOException e) {
            _log.error(e);
            return false;
        }
        return true;
    }

    public ElementCachedImage(BufferedImage bi) {
        if (bi == null) throw new NullPointerException();
        img = new ImageWrapper(bi);
        PhantomReference<ElementCachedImage> pref = 
                new PhantomReference<ElementCachedImage>(this, refQue);
        refMap.put(pref, img.getFile());

        new Thread("Save image to file") {
            @Override
            public void run() {
                synchronized(ElementCachedImage.this) {
                    if (img != null) {
                        img.saveToFile();
                        img.getFile().deleteOnExit();
                    }
                }
            }
        }.start();
    }
}

Some filtered output:

2013-08-05 22:35:01,932 DEBUG Save image to file: <>\AppData\Local\Temp\tmp7..0.PNG

2013-08-05 22:35:03,379 DEBUG Deleting unused file: <>\AppData\Local\Temp\tmp7..0.PNG created at Mon Aug 05 22:35:02 IDT 2013

Disinfest answered 5/8, 2013 at 19:2 Comment(8)
You don't actually enqueue anything.Winshell
So I should call enqueue()... got it, thanks!Disinfest
See my answer, last line.Winshell
FWIW, you should not assume your finalization will ever get called. See blogs.msdn.com/b/oldnewthing/archive/2010/08/09/10047586.aspx (relevant to Java too)Strati
Of course, I only use it as a safety net. hence the createTemporaryFile() and the deleteOnExit() in my code... Anyway, thanks for a great read!Disinfest
as i said in your other question, this example is not thread-safe (HashMap is not thread-safe). you have at least solved the memory issue. as i noted in my other comment, you don't need the extra map, just maintain the Reference in the ElementCachedImage.Guru
Your probably right (I'll be more confident after testing it...)Disinfest
If this is just a safety net, i would get rid of it. it just slows GC down. How about adding a method to your cache which removes the file on disc when the entry expires? no magic involved, can be invoked manually when required.Travax
M
6

The answer is, that in your example the PhantomReference itself is unreachable and hence garbage collected before the referred object itself is garbage collected. So at the time the object is GCed there is no more Reference and the GC does not know that it should enqueue something somewhere.

This of course is some kind of head-to-head race :-)

This also explains (without looking to deep into your new code) why putting the reference into some reachable collection makes the example work.

Just for reference (pun intended) here is a modified version of your first example which works (on my machine :-) I just added a set holding all references.

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashSet;
import java.util.Set;

public class DeathNotificationObject {
    private static ReferenceQueue<DeathNotificationObject> refQueue = new ReferenceQueue<DeathNotificationObject>();
    private static Set<Reference<DeathNotificationObject>> refs = new HashSet<>();

    static {
        Thread deathThread = new Thread("Death notification") {
            @Override
            public void run() {
                try {
                    while (true) {
                        Reference<? extends DeathNotificationObject> ref = refQueue.remove();
                        refs.remove(ref);
                        System.out.println("I'm dying!");
                    }
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
        };
        deathThread.setDaemon(true);
        deathThread.start();
    }

    public DeathNotificationObject() {
        System.out.println("I'm born.");
        PhantomReference<DeathNotificationObject> ref = new PhantomReference<DeathNotificationObject>(this, refQueue);
        refs.add(ref);
    }

    public static void main(String[] args) {
        for (int i = 0 ; i < 10 ; i++) {
            new DeathNotificationObject();                  
        }
        try {
            System.gc();    
            Thread.sleep(3000); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Update

Calling enqueue by hand is possible in your example but not in real code. it gives plain wrong result. Let me show by calling enqueue in the constructor and using another main:

public DeathNotificationObject() {
    System.out.println("I'm born.");
    PhantomReference<DeathNotificationObject> ref = new PhantomReference<DeathNotificationObject>(this, refQueue);
    ref.enqueue();
}

public static void main(String[] args) throws InterruptedException {

    for (int i = 0 ; i < 5 ; i++) {
        DeathNotificationObject item = new DeathNotificationObject();

        System.out.println("working with item "+item);
        Thread.sleep(1000);
        System.out.println("stopped working with item "+item);
        // simulate release item
        item = null;
    }

    try {
        System.gc();    
        Thread.sleep(3000); 
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

The output will be like this:

I'm born.
I'm dying!
working with item DeathNotificationObject@6908b095
stopped working with item DeathNotificationObject@6908b095

Which means that whatever you wanted to do with the reference queue would be done when the item is still alive.

Micra answered 5/8, 2013 at 20:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.