Clean your Memory: From Finalize to Cleaner
- March 31, 2025
- 674 Unique Views
- 5 min read
Garbage collection in Java takes care of memory management, but it does not clean up non-memory resources like sockets or file handles.
Resource leaks may occur without proper management, leading to performance degradation or crashes.
Java’s Cleaner API, introduced in Java 9, provides a modern and efficient mechanism for resource cleanup when objects are no longer reachable.
It addresses the shortcomings of the deprecated finalize() method, offering a predictable and efficient way to manage non-memory resources: so let's take a short trip on clean memory from finalize to Cleaner API.
Why is the finalize() method deprecated/removed?
The finalize() method was initially introduced in Java to provide a way for objects to perform cleanup actions before they are garbage collected. It belongs to java.lang.Object
and can be overridden by subclasses to release resources like file handles or network sockets. Why was this approach problematic?
- Unpredictable Execution: The
finalize()
method is invoked at an undetermined time when GC decides to collect the object. - Performance Overhead: Objects with a
finalize()
method take longer to collect, as they must go through an extra GC cycle. - Memory Leaks: If an object is unintentionally retained (e.g., due to an exception in
finalize()
), it may never be garbage collected. - Finalization Queue Mechanism:
finalize()
execution happens in a separate thread(the Finalizer Thread), which can lead to thread contention and delays.
How does Cleaner relate to Java Reference classes?
In Java, there are four types of references differentiated by the way by which they are garbage collected:
- Strong References: objects having an active, strong reference are not eligible for garbage collection. The object is garbage collected only when the variable, which was strongly referenced, points to null.
- Weak References: the objects referenced by a weak reference do not prevent their referents from being made finalizable, finalized, and reclaimed.
- Soft References: the objects which are being referenced by soft reference, even if the object is free for garbage collection, it’s not garbage collected until JVM needs memory badly
- Phantom References: the objects referenced by phantom references are eligible for garbage collection, but before removing them, the JVM puts them in the "reference queue".
The logic that guides how they are collected is always related to the concept of reachability, as described in the related Javadoc. Java Reference classes are not easy to use, and the resulting code is sometimes complicated.
We must also consider that in the case of a Phantom Reference, once the Referent is registered, the reference always returns null
, making it seem useless, but it is not!
Here’s an example of PhantomReference
:
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 { public void cleaning() { System.out.println("cleaning"); } } record ResourceHolder(Resource resource) {} private static final Map lookup = new HashMap(); private static final ReferenceQueue queue = new ReferenceQueue(); public static void main(String[] args) { var holder = new ResourceHolder(new Resource()); lookup.put(new PhantomReference(holder, queue), holder.resource()); holder = null; System.gc(); Reference element = null; while ((element = queue.poll()) == null) { System.out.println("wating for GC"); } System.out.println("GCollected!"); lookup.remove(element).cleaning(); } }
Cleaner
shares similarities with Java’s reference classes, but it is more efficient for managing external resources. While PhantomReference enables cleanup logic, Cleaner
adds an abstraction layer, making it easier to implement and manage. Internally, Cleaner
uses a phantom reference behind the scenes to detect when an object becomes unreachable.
Here’s an example of Cleaner
:
import java.lang.ref.Cleaner; public class BasicCleanerExample { 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 { private final Cleaner.Cleanable cleanable; ManagedObject() { cleanable = cleaner.register(this, new CleaningAction()); } } public static void main(String[] args) { new ManagedObject(); System.gc(); pause(); } }
Behind the scenes of Cleaner
The implementation behind Cleaner is not trivial. It leverages a combination of the Java PhantomReference
and a background daemon thread. Let’s explore the inner workings:
- Registration with Cleaner: an object registered with a
Cleaner
is associated with aCleanable
task. The object is essentially "watched" by a background daemon thread. - Phantom Reference Management: Under the hood,
Cleaner
uses aPhantomReference
to track the object’s reachability. - Daemon Thread for Cleanup: The Cleaner framework uses a dedicated daemon thread (normally named Common-Cleaner) that monitors registered objects. Once an object becomes unreachable, the cleanup task for that object is queued for execution in this background thread.
The following snippet uses a Cleaner
to manage files.
import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.lang.ref.Cleaner; public class FileCleanerExample { private static final Cleaner cleaner = Cleaner.create(); static class FileResource implements Runnable { private final File file; FileResource(File file) { this.file = file; System.out.printf("resource create with temporary file: %s%n", file.getAbsolutePath()); } @Override public void run() { if (file.exists()) { System.out.printf("temporary file deleted: %s%n",file.delete()); } } } static class ManagedFile { private Cleaner.Cleanable cleanable; ManagedFile(String path) throws IOException { var file = new File(path); cleanable = cleaner.register(this, new FileResource(file)); new FileWriter(file).write("Temporary data"); } } public static void main(String[] args) throws IOException { var managed = new ManagedFile("temp.txt"); managed = null; System.gc(); pause(); } // ... }
Cleaner vs. try-with-resources
The example with a file might suggest a more idiomatic solution using the try-with-resources
construct. It may be a real solution but is less so if we consider particular resources such as images or memory buffers directly mapped to memory. As a last note, we also need to consider that Autocloseable
can be used with Cleanable
, invoking the clean()
method from the close()
one, having the same effect as a GC operation.
Here’s a dedicated example.
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 object = new ManagedObject(); try(object){ System.out.printf("using: %s%n",object); } } }
The following table allows us to summarize the tools presented and choose the one that best suits our purposes.
Feature | try-with-resources | Cleaner |
---|---|---|
Resource Type |
Autocloseable |
It also works with no Autocloaseable object |
Cleanup Timing |
Immediate |
Deferred, asynchronous |
Use Case |
When precise cleanup timing is required |
When working with external resources or objects without explicit close methods |
Overhead |
Minimal |
Higher due to the background thread |
Avoid overusing Cleaner
Using Cleaner
is more straightforward than using references. Still, we must exercise caution when using this kind of resource: only use Cleaner
when the runtime can’t release resources via try-with-resources
or explicit close()
calls.
We also need to consider that we create new elements, and sometimes it’s better to use one Cleaner
for multiple resources or to listen to the cleaning action.
More aspects need our attention, as for cleaning action, following some rules:
- Avoid using lambda because it risks capturing the object reference by referring to fields of the object being cleaned, preventing the object from becoming phantom reachable.
- Cleaning actions can be invoked concurrently with other cleaning actions. They should be swift to execute and not block: it may delay processing other cleaning actions registered to the same cleaner.
- We can also consider executing a cleaning action and delegating it to another thread pool, but this idea can involve more complexity and not solve concurrency issues.
Conclusion
Java’s Cleaner API provides a modern, efficient approach to resource management, addressing the limitations of finalize()
. By leveraging Cleaner
, developers can ensure that non-AutoCloseable
resources, such as native memory and caches, are properly cleaned up when no longer needed. However, you should always prefer try-with-resources when possible for deterministic resource management.
By understanding when and how to use Cleaner
, developers can write more reliable, high-performance, and memory-efficient Java applications. Mastering these best practices will be essential for building robust, scalable software as Java evolves.
Further Resources
- How to handle Java errors and cleanup without finalize
- Replacing Finalizers with Cleaners
- Java Cleaners: The Modern Way to Manage External Resources
- Cleaner code practices for Java programming
Don’t Forget to Share This Post!
Comments (0)
No comments yet. Be the first.