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.
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‑resourcesprovides 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Cognitive Technology Team
Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
