3

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();
        }
    }
}
dan1st
  • 12,568
  • 8
  • 34
  • 67
  • 1
    Have you had a look at the chapter in Effective Java devoted to the appropriateness of finalizers and cleaners? – that other guy Jan 17 '21 at 21:42
  • 1
    This is why I wrote this question, yes. – dan1st Jan 17 '21 at 21:43
  • Are you concerned that mixing "automatic" and "manual" closing will cause a problem? And note since Java 9 you should always prefer `java.lang.ref.Cleaner` over `finalize` (the latter is deprecated). – Slaw Jan 17 '21 at 22:07
  • I know about the deprecation but I didn't know the exact usage of `Cleaner` by heart and finalizers are sufficient for the examples (at least, I thought so). I am not especially concerned about mixing the closings but rather if this method is safe (will get closed in every case). – dan1st Jan 17 '21 at 22:10
  • @Slaw that is not going to be easy, nor trivial. You need to invoke a method on an instance that you register with `Cleaner`, which means you will _capture_ that instance, holding a strong reference now; which means it becomes useless... you will need to use a `PhantomReference` directly, most probably. I will try to post an answer soon, if no one else does by then. – Eugene Jan 18 '21 at 05:11
  • @Eugene Not sure I understand the problem. You specifically _don't_ capture a strong reference to the instance being registered with the `Cleaner`. The documentation even gives an example. – Slaw Jan 18 '21 at 05:34
  • @Slaw right. What about the second argument of `register`? If you do `...register(instance, instance::close)`, you have effectively captured `instance`, via the _second_ argument. The first is indeed managed internally via a `PhantomReference`. Does this make sense? – Eugene Jan 18 '21 at 18:28
  • @Eugene Yes, but that's _specifically why you don't capture the `instance` in the second argument_. There's absolutely no reason to. You should only pass the resource to the "cleaning action". Then the `close()` method of `instance` calls the `clean()` method of the returned `Cleanable`. – Slaw Jan 18 '21 at 19:34
  • @Slaw exactly. So the OP (if possible, should refactor the code) should create a separate inner static class to clean-up, or use a few workarounds if that is not an option. Use `PhantonReference`s directly, or hack it with a static initializer. I am yet to work on an answer, if time permits today/tomorrow. – Eugene Jan 18 '21 at 19:36
  • @Eugene I feel like maybe I'm missing something. You seem to agree with me, but I don't understand what you mean by "_that is not going to be easy, nor trivial_". I suppose I don't see what's not easy about refactoring `finalize` to using `Cleaner` (or at least I don't see it as prohibitively complex). I am also of the opinion that implementing a solution with `Cleaner` is likely the easiest option (e.g. `finalize` has way more pitfalls). – Slaw Jan 18 '21 at 19:40
  • 1
    @Slaw sorry about that. the untrivial part comes from the fact that it is too easy to just code it as `register(instance, instance::run)`. that is the only thing I wanted to highlight. – Eugene Jan 18 '21 at 19:42
  • Note that I asked the question about whether finalizers/cleaners are appropriate for this and not how to refactor code with a finalizer to code with a cleaner. – dan1st Jan 18 '21 at 19:45
  • 1
    Understood. @Eugene and I were sort of having our own little tangent. But if Eugene does give an answer it doesn't hurt to showcase the current best practices. – Slaw Jan 18 '21 at 19:47
  • @Slaw I will take your critics now please, if you have any – Eugene Jan 19 '21 at 21:02

2 Answers2

1

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
Eugene
  • 117,005
  • 15
  • 201
  • 306
  • Thanks for the information but (as I mentioned in the questions and the comments), the question was whether finalizers/cleaners are appropriate and not how to use cleaners instead of finalizers. – dan1st Jan 19 '21 at 20:52
  • @dan1st I did address that in the very beginning, yes. The rest is just addendum. – Eugene Jan 19 '21 at 20:57
0

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.

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • Those notions of `server` and `client` are long gone. Besides closing everything explicitly is not always an option. – Eugene Jan 18 '21 at 05:27
  • @Eugene thanks for the information, I fixed the part about `-client` and `-server`. Closing everything explicitly must always be an option, because the alternative is software that just does not work properly. – Mike Nakis Jan 18 '21 at 06:08
  • @Eugene as a matter of fact, if you would agree, I would love to debate the last statement of your comment (and the last statement of my comment.) I recently wrote this: https://blog.michael.gr/2021/01/object-lifetime-awareness.html and I could really use some feedback from a knowledgeable software engineer such as you. We could discuss it either in the chat rooms here on SO or by other means. – Mike Nakis Jan 18 '21 at 06:26
  • So how should I close objects where I only want to have soft/weak references? – dan1st Jan 18 '21 at 07:39
  • @dan1st I have no answer for you. I would begin by reconsidering why you want to use soft/weak references, because they work as if by magic, and they rarely behave the way we expect them to. So, perhaps you should begin with what you originally wanted to accomplish, and ask how to accomplish this, instead of deciding that you need weak/soft references to accomplish this, and asking about potential problems with those. My answer here is that there will be problems with those. – Mike Nakis Jan 18 '21 at 08:43
  • 2
    It should be noted that the actual behavior does not need to be one of the two bullets—it can be both at the same time. The garbage collector may aggressively collect several of the entities, followed by being happy with the result for a long time, not collecting other instances. This may result in the youngest objects being immediately collected while the oldest stay in memory. – Holger Jan 18 '21 at 15:54