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