bbnt
bbnt

Reputation: 340

Java: Implementing non-empty finalize() makes cleaner never run

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

Answers (1)

apangin
apangin

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:

  1. During the first GC, JVM discovers that there are no strong/soft/weak references to the object, but the object has non-trivial 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.
  2. 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.
  3. After the finalize method has been invoked for an object, no further action is taken until the Java virtual machine has again determined that there is no longer any means by which this object can be accessed by any thread <...> (see Object.finalize). This means, JVM does not detect if an object becomes phantom reachable until the next GC.
  4. Only during the next GC cycle, JVM discovers that the object has become finalized and thus a cleaning action can be invoked.

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

Related Questions