Reputation: 340
I'm implementing a JNI wrapper class that cleans up native handles. We need to support Java 8-17, so we're using both finalize (for Java 8) and Cleaner (for Java 9+). I'm going to load the cleaner via reflection and only fallback to the finalize method when its java 8 so only one will be executed. However, I'm seeing unexpected behavior when implementing that in the same class.
If finalize() is empty, the Cleaner runs as expected. But if finalize() contains any code (even just a System.out.println), the Cleaner never runs. Why does this happen?
Here's a minimal example. If the println is commented out, it passes. If not, it fails
public abstract class Cleanable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private Runnable cleanAction;
public Cleanable(Runnable inputCleanAction) {
cleanable = cleaner.register(this, inputCleanAction);
this.cleanAction = inputCleanAction;
}
@Override
public void finalize() throws Throwable {
\\ System.out.println("finalize called");
}
}
AtomicInteger cleaned = new AtomicInteger(0);
{
Cleanable cleanable = new Cleanable(() -> cleaned.incrementAndGet()) {};
cleanable = null;
}
System.gc();
Thread.sleep(1000);
Assert.assertEquals(1, cleaned.get());
Holding the reference to the cleanAction as an instance variable is not the problem because this code works as long as the finalize body is empty. As soon as I do anything inside of finalize, it fails.
Upvotes: 1
Views: 63
Reputation: 98600
But if finalize() contains any code (even just a System.out.println), the Cleaner never runs.
This statement is not quite correct. Cleaner does run, but only after it gets notified that the object has become phantom reachable (see Class Cleaner).
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. [emphasis added]
Non-trivial finalize
method extends lifetime of an object in the following way:
finalize
, and puts this object into the finalization queue. The object is not finalized yet at this point, since its finalizer has not been executed yet.finalize
, being a Java method, runs on a usual Java thread, not on a GC thread. Depending on GC algorithm, it runs after GC cycle completes or concurrently with GC.So, to see a cleaning action executed for a finalizable object, you need to wait for at least two GC cycles. That's another reason why finalizers are considered bad - they extend unreachable object's lifetime for at least one extra GC cycle.
Upvotes: 6