1. Overview
In this tutorial, we’ll focus on a core aspect of the Java language – the finalize method provided by the root Object class.
Simply put, this is called before the garbage collection for a particular object.
2. Using Finalizers
The finalize() method is called the finalizer.
Finalizers get invoked when JVM figures out that this particular instance should be garbage collected. Such a finalizer may perform any operations, including bringing the object back to life.
The main purpose of a finalizer is, however, to release resources used by objects before they’re removed from the memory. A finalizer can work as the primary mechanism for clean-up operations, or as a safety net when other methods fail.
To understand how a finalizer works, let’s take a look at a class declaration:
public class Finalizable {
private BufferedReader reader;
public Finalizable() {
InputStream input = this.getClass()
.getClassLoader()
.getResourceAsStream("file.txt");
this.reader = new BufferedReader(new InputStreamReader(input));
}
public String readFirstLine() throws IOException {
String firstLine = reader.readLine();
return firstLine;
}
// other class members
}
The class Finalizable has a field reader, which references a closeable resource. When an object is created from this class, it constructs a new BufferedReader instance reading from a file in the classpath.
Such an instance is used in the readFirstLine method to extract the first line in the given file. Notice that the reader isn’t closed in the given code.
We can do that using a finalizer:
@Override
public void finalize() {
try {
reader.close();
System.out.println("Closed BufferedReader in the finalizer");
} catch (IOException e) {
// ...
}
}
It’s easy to see that a finalizer is declared just like any normal instance method.
In reality, the time at which the garbage collector calls finalizers is dependent on the JVM’s implementation and the system’s conditions, which are out of our control.
To make garbage collection happen on the spot, we’ll take advantage of the System.gc method. In real-world systems, we should never invoke that explicitly, for a number of reasons:
- It’s costly
- It doesn’t trigger the garbage collection immediately – it’s just a hint for the JVM to start GC
- JVM knows better when GC needs to be called
If we need to force GC, we can use jconsole for that.
The following is a test case demonstrating the operation of a finalizer:
@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
String firstLine = new Finalizable().readFirstLine();
assertEquals("baeldung.com", firstLine);
System.gc();
}
In the first statement, a Finalizable object is created, then its readFirstLine method is called. This object isn’t assigned to any variable, hence it’s eligible for garbage collection when the System.gc method is invoked.
The assertion in the test verifies the content of the input file and is used just to prove that our custom class works as expected.
When we run the provided test, a message will be printed on the console about the buffered reader being closed in the finalizer. This implies the finalize method was called and it has cleaned up the resource.
Up to this point, finalizers look like a great way for pre-destroy operations. However, that’s not quite true.
In the next section, we’ll see why using them should be avoided.
3. Avoiding Finalizers
Despite the benefits they bring in, finalizers come with many drawbacks.
3.1. Disadvantages of Finalizers
Let’s have a look at several problems we’ll be facing when using finalizers to perform critical actions.
The first noticeable issue is the lack of promptness. We cannot know when a finalizer runs since garbage collection may occur anytime.
By itself, this isn’t a problem because the finalizer still executes, sooner or later. However, system resources aren’t unlimited. Thus, we may run out of resources before a clean-up happens, which may result in a system crash.
Finalizers also have an impact on the program’s portability. Since the garbage collection algorithm is JVM implementation-dependent, a program may run very well on one system while behaving differently on another.
The performance cost is another significant issue that comes with finalizers. Specifically, JVM must perform many more operations when constructing and destroying objects containing a non-empty finalizer.
The last problem we’ll be talking about is the lack of exception handling during finalization. If a finalizer throws an exception, the finalization process stops, leaving the object in a corrupted state without any notification.
3.2. Demonstration of Finalizers’ Effects
It’s time to put the theory aside and see the effects of finalizers in practice.
Let’s define a new class with a non-empty finalizer:
public class CrashedFinalizable {
public static void main(String[] args) throws ReflectiveOperationException {
for (int i = 0; ; i++) {
new CrashedFinalizable();
// other code
}
}
@Override
protected void finalize() {
System.out.print("");
}
}
Notice the finalize() method – it just prints an empty string to the console. If this method were completely empty, the JVM would treat the object as if it didn’t have a finalizer. Therefore, we need to provide finalize() with an implementation, which does almost nothing in this case.
Inside the main method, a new CrashedFinalizable instance is created in each iteration of the for loop. This instance isn’t assigned to any variable, hence eligible for garbage collection.
Let’s add a few statements at the line marked with // other code to see how many objects exist in the memory at runtime:
if ((i % 1_000_000) == 0) {
Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
Field queueStaticField = finalizerClass.getDeclaredField("queue");
queueStaticField.setAccessible(true);
ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queueStaticField.get(null);
Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength");
queueLengthField.setAccessible(true);
long queueLength = (long) queueLengthField.get(referenceQueue);
System.out.format("There are %d references in the queue%n", queueLength);
}
The given statements access some fields in internal JVM classes and print out the number of object references after every million iterations.
Let’s start the program by executing the main method. We may expect it to run indefinitely, but that’s not the case. After a few minutes, we should see the system crash with an error similar to this:
...
There are 21914844 references in the queue
There are 22858923 references in the queue
There are 24202629 references in the queue
There are 24621725 references in the queue
There are 25410983 references in the queue
There are 26231621 references in the queue
There are 26975913 references in the queue
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.ref.Finalizer.register(Finalizer.java:91)
at java.lang.Object.<init>(Object.java:37)
at com.baeldung.finalize.CrashedFinalizable.<init>(CrashedFinalizable.java:6)
at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9)
Process finished with exit code 1
Looks like the garbage collector didn’t do its job well – the number of objects kept increasing until the system crashed.
If we removed the finalizer, the number of references would usually be 0 and the program would keep running forever.
3.3. Explanation
To understand why the garbage collector didn’t discard objects as it should, we need to look at how the JVM works internally.
When creating an object, also called a referent, that has a finalizer, the JVM creates an accompanying reference object of type java.lang.ref.Finalizer. After the referent is ready for garbage collection, the JVM marks the reference object as ready for processing and puts it into a reference queue.
We can access this queue via the static field queue in the java.lang.ref.Finalizer class.
Meanwhile, a special daemon thread called Finalizer keeps running and looks for objects in the reference queue. When it finds one, it removes the reference object from the queue and calls the finalizer on the referent.
During the next garbage collection cycle, the referent will be discarded – when it’s no longer referenced from a reference object.
If a thread keeps producing objects at a high speed, which is what happened in our example, the Finalizer thread cannot keep up. Eventually, the memory won’t be able to store all the objects, and we end up with an OutOfMemoryError.
Notice a situation where objects are created at warp speed as shown in this section doesn’t often happen in real life. However, it demonstrates an important point – finalizers are very expensive.
4. No-Finalizer Example
Let’s explore a solution providing the same functionality but without the use of finalize() method. Notice that the example below isn’t the only way to replace finalizers.
Instead, it’s used to demonstrate an important point: there are always options that help us to avoid finalizers.
Here’s the declaration of our new class:
public class CloseableResource implements AutoCloseable {
private BufferedReader reader;
public CloseableResource() {
InputStream input = this.getClass()
.getClassLoader()
.getResourceAsStream("file.txt");
reader = new BufferedReader(new InputStreamReader(input));
}
public String readFirstLine() throws IOException {
String firstLine = reader.readLine();
return firstLine;
}
@Override
public void close() {
try {
reader.close();
System.out.println("Closed BufferedReader in the close method");
} catch (IOException e) {
// handle exception
}
}
}
It’s not hard to see that the only difference between the new CloseableResource class and our previous Finalizable class is the implementation of the AutoCloseable interface instead of a finalizer definition.
Notice that the body of the close method of CloseableResource is almost the same as the body of the finalizer in class Finalizable.
The following is a test method, which reads an input file and releases the resource after finishing its job:
@Test
public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
try (CloseableResource resource = new CloseableResource()) {
String firstLine = resource.readFirstLine();
assertEquals("baeldung.com", firstLine);
}
}
In the above test, a CloseableResource instance is created in the try block of a try-with-resources statement, hence that resource is automatically closed when the try-with-resources block completes execution.
Running the given test method, we’ll see a message printed out from the close method of the CloseableResource class.
5. Conclusion
In this tutorial, we focused on a core concept in Java – the finalize method. This looks useful on paper but can have ugly side effects at runtime. And, more importantly, there’s always an alternative solution to using a finalizer.
One critical point to notice is that finalize has been deprecated starting with Java 9 – and will eventually be removed.
As always, the source code for this tutorial can be found over on GitHub.