Fundamentals 12 min read

How Java’s Cleaner API Replaces finalize() for Safer Resource Management

This article explains why finalize() was deprecated, introduces the Java Cleaner API, compares it with reference types and try‑with‑resources, provides practical code examples, and offers guidelines to use Cleaner effectively while avoiding common pitfalls.

Cognitive Technology Team
Cognitive Technology Team
Cognitive Technology Team
How Java’s Cleaner API Replaces finalize() for Safer Resource Management

Why the finalize() method was deprecated

The finalize() mechanism suffered from several fundamental problems:

Unpredictable execution time : The garbage collector decides when to invoke finalize(), which can be arbitrarily delayed.

Performance overhead : Objects that override finalize() require an extra GC cycle, increasing pause times.

Potential memory leaks : If finalize() throws an exception or resurrects the object, the object may never become reclaimable.

Finalizer thread contention : Finalization runs on a single background thread, creating a bottleneck and additional latency.

Java reference types

Java defines four reference classes that influence reachability and collection:

StrongReference : Prevents collection as long as a strong reference exists.

WeakReference : Does not prevent collection; cleared as soon as the referent becomes weakly reachable.

SoftReference : Cleared only when the JVM needs memory, useful for caches.

PhantomReference : Enqueued after the referent is reclaimed; the referent is always null when accessed, making it ideal for post‑mortem cleanup.

Using PhantomReference for manual cleanup

The following example demonstrates a typical pattern:

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;

public class UsingPhantomRef {
    static class Resource {
        void cleaning() { System.out.println("cleaning"); }
    }
    record ResourceHolder(Resource resource) {}

    private static final Map<PhantomReference<ResourceHolder>, Resource> lookup = new HashMap<>();
    private static final ReferenceQueue<? super ResourceHolder> queue = new ReferenceQueue<>();

    public static void main(String[] args) throws InterruptedException {
        // (1) Create a holder that owns the real resource
        var holder = new ResourceHolder(new Resource());
        // (2) Register a phantom reference and keep a map from the reference to the resource
        lookup.put(new PhantomReference<ResourceHolder>(holder, queue), holder.resource());
        // (3) Make the holder unreachable and request a GC for demonstration
        holder = null;
        System.gc();
        // (4) Wait until the reference is enqueued
        Reference<?> element;
        while ((element = queue.poll()) == null) {
            Thread.sleep(10);
        }
        System.out.println("GCollected!");
        // (5) Retrieve the real resource and invoke its cleanup logic
        lookup.remove(element).cleaning();
    }
}

Steps 1‑5 correspond to the comments in the code. The map is necessary because a phantom reference’s get() always returns null.

Cleaner API (Java 9+)

The java.lang.ref.Cleaner class builds on phantom references but provides a higher‑level abstraction:

import java.lang.ref.Cleaner;

public class BasicCleanerExample {
    // (1) Create a singleton Cleaner; it starts a daemon thread internally
    private static final Cleaner cleaner = Cleaner.create();

    // (2) Define the cleanup action
    static class CleaningAction implements Runnable {
        @Override public void run() { System.out.println("Resource cleaned up!"); }
    }

    // (3) Register the object and obtain a Cleanable handle
    static class ManagedObject {
        private final Cleaner.Cleanable cleanable;
        ManagedObject() { cleanable = cleaner.register(this, new CleaningAction()); }
    }

    public static void main(String[] args) throws InterruptedException {
        // (4) Create the managed object; registration happens immediately
        new ManagedObject();
        // (5) Suggest a GC to trigger the cleanup for demo purposes
        System.gc();
        // (6) Give the cleaner thread time to run (e.g., Thread.sleep)
        Thread.sleep(100);
    }
}

The Cleaner maintains its own daemon thread that watches a ReferenceQueue of phantom references. When a registered object becomes phantom‑reachable, the associated Runnable is queued and later executed by the cleaner thread.

Cleaner implementation details

Objects are registered with Cleaner.register(Object, Runnable), which returns a Cleaner.Cleanable handle.

Internally a PhantomReference to the target object is created and placed on a private ReferenceQueue.

A dedicated daemon thread continuously polls the queue; for each enqueued reference it invokes the corresponding Runnable.

The cleanable can be invoked manually via cleanable.clean(), which runs the action immediately and deregisters the reference.

Cleaner vs. try‑with‑resources

try‑with‑resources

provides deterministic cleanup for objects that implement AutoCloseable. Cleaner is useful when:

The resource does not have a close method (e.g., native memory buffers, temporary files).

Deterministic cleanup is not required, and a delayed asynchronous cleanup is acceptable.

Cleaner can be combined with AutoCloseable by calling cleanable.clean() inside close():

import java.lang.ref.Cleaner;

public class CleanerWithCloseExample {
    private static final Cleaner cleaner = Cleaner.create();

    static class CleaningAction implements Runnable {
        @Override public void run() { System.out.println("Resource cleaned up!"); }
    }

    static class ManagedObject implements AutoCloseable {
        private final Cleaner.Cleanable cleanable;
        ManagedObject() { cleanable = cleaner.register(this, new CleaningAction()); }
        @Override public void close() { System.out.println("close invoked!"); cleanable.clean(); }
    }

    public static void main(String[] args) {
        var obj = new ManagedObject();
        try (obj) { System.out.printf("using: %s%n", obj); }
    }
}

Guidelines for using Cleaner responsibly

Avoid lambdas or inner classes that capture the target object; such captures create strong references that prevent phantom reachability.

Keep cleanup actions short, non‑blocking, and free of I/O; heavy work should be handed off to an executor service.

Prefer deterministic mechanisms ( try‑with‑resources or explicit close()) whenever possible; use Cleaner only when those mechanisms cannot be applied.

Conclusion

The Java Cleaner API offers a modern, low‑overhead way to release non‑memory resources after an object becomes unreachable, addressing the unpredictability and performance penalties of finalize(). While Cleaner simplifies cleanup for native buffers, temporary files, or other external resources, developers should still favor deterministic patterns such as try‑with‑resources for predictable resource management.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaResource ManagementGarbage Collectionfinalizereferencescleaner
Cognitive Technology Team
Written by

Cognitive Technology Team

Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.