dan1st
dan1st

Reputation: 16486

cleaning up weak reference caches with finalizers?

Assume I have a cache that consists of weak or soft references.

Those weak/soft references need to be closed at some point.

Ideally, the objects should be closed as soon as the objects are removed from the cache by the GC.

Is it appropriate to use a finalizer/cleaner to close those resources while still looping over the cache at the end of the programand closing them manually?

public void CachedObject implements AutoClosable{
    private boolean open;//getter
    public CachedObject{
        //Create resource
        open=true;
    }
    @Override
    public void finalize(){
        super.finalize();
        if(open){
            try{
                close();
             }catch(IllegalStateException e){
                 //Log
            }
        }
    }
    @Override
    public void close(){
        if(open){
            //Close
            open=false;
        }else{
            throw new IllegalStateException("already closed");
        }
    }
}
private WeakHashMap<CachedObject,Object> cache=new WeakHashMap<>();

public void close(){
    //Executed when cache is not needed anymore, e.g. program termination
    for(CachedObject cachedElement:cache){
        if(cachedElement.isOpen()){
             cachedElement.close();
        }
    }
}

Upvotes: 3

Views: 661

Answers (2)

Eugene
Eugene

Reputation: 121078

It is a rather bad idea to use finalizer, in general; it was deprecated for a reason, after all. I think that first it is important to understand the mechanics on how such a special method works to begin with, or why it takes two cycles for an Object that implements finalizer to be gone. The overall idea is that this is non-deterministic, easy to get wrong and you might face un-expected problems with such an approach.

The de facto way to clean-up something is to use try with resources (via AutoCloseable), as easy as :

CachedObject cached = new CachedObject...
try(cached) {

} 

But that is not always an option, just like in your case, most probably. I do not know what cache you are using, but we internally use our own cache, which implements a so called removal listener (our implementation is HEAVILY based on guava with minor additions of our own). So may be your cache has the same? If not, may be you can switch to one that does?

If neither is an option, there is the Cleaner API since java-9. You can read it, and for example do something like this:

static class CachedObject implements AutoCloseable {

    private final String instance;

    private static final Map<String, String> MAP = new HashMap<>();

    public CachedObject(String instance) {
        this.instance = instance;
    }

    @Override
    public void close()  {
        System.out.println("close called");
        MAP.remove(instance);
    }
}

And then try to use it, via:

private static final Cleaner CLEANER = Cleaner.create();

public static void main(String[] args) {

    CachedObject first = new CachedObject("first");
    CLEANER.register(first, first::close);
    first = null;
    gc();
    System.out.println("Done");

}

static void gc(){
    for(int i=0;i<3;++i){
        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
        System.gc();
    }
}

Easy, right? Also wrong. The apiNote mentions this via:

The cleaning action is invoked only after the associated object becomes phantom reachable, so it is important that the object implementing the cleaning action does not hold references to the object

The problem is that Runnable (in the second argument of Cleaner::register) captures first, and now holds a strong reference to it. This means that the cleaning will never be called. Instead, we can directly follow the advice in the documentation:

static class CachedObject implements AutoCloseable {

    private static final Cleaner CLEANER = Cleaner.create();
    private static final Map<String, String> MAP = new HashMap<>();
    private final InnerState innerState;
    private final Cleaner.Cleanable cleanable;

    public CachedObject(String instance) {
        innerState = new InnerState(instance);
        this.cleanable = CLEANER.register(this, innerState);
        MAP.put(instance, instance);
    }

    static class InnerState implements Runnable {

        private final String instance;

        public InnerState(String instance) {
            this.instance = instance;
        }

        @Override
        public void run() {
            System.out.println("run called");
            MAP.remove(instance);
        }
    }

    @Override
    public void close()  {
        System.out.println("close called");
        cleanable.clean();
    }
}

The code looks a bit involved, but in reality it is not that much. We want to do two main thing:

  • separate the code for the cleaning in a separate class
  • and that class has to have no reference to the object we are registering. This is achieved by having no references from InnerState to CachedObject and also making it static.

So, we can test that:

 public static void main(String[] args) {

    CachedObject first = new CachedObject("first");
    first = null;
    gc();

    System.out.println("Done");
    System.out.println("Size = " + CachedObject.MAP.size());


 }

 static void gc() {
    for(int i=0;i<3;++i){
        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
        System.gc();
    }
 }

Which will output:

run called
Done
Size = 0

Upvotes: 1

Mike Nakis
Mike Nakis

Reputation: 62159

Garbage disposal (and therefore finalization) is non-deterministic and quite capricious, so I would recommend that you do not base any important function of your software on it.

There are no guarantees as to when your objects will be finalized (or cleaned) nor as to even whether they will be finalized. Just try to completely avoid it for any purpose other than perhaps diagnostics, i.e. to generate a warning-level log message telling you that an object is being finalized without first having been closed. But you better close everything explicitly.

The idea of cached entities being evicted from the cache when the machine needs more memory sounds beautiful at first, but in reality you will discover that:

  • if your garbage collector works aggressively, (default behavior on 64-bit JVMs) your entities will be evicted far more frequently than you had hoped, (way before you start running out of memory,) while

  • if your garbage collector works lazily, (depending on JVM startup options,) your app might run to completion without exhausting its available memory and then the JVM may terminate without finalizing or cleaning anything.

Upvotes: 0

Related Questions