Advanced Java Garbage Collection Concepts: Weak References, Finalization, and Memory Leaks
Weak references are not always the answer to memory leaks, unless combined with good programming practices. What happens when there's an issue with the finalize() method?
Join the DZone community and get the full member experience.
Join For FreeThe WeakReference()
class in Java is often touted as being the answer to memory leaks. However, weak references on their own are not necessarily the answer.
Memory leaks are one of the hardest issues to diagnose. This article looks at a scenario where using weak references in conjunction with an object’s finalize()
method can result in a memory leak.
If you're not familiar with the way the garbage collector works in Java, I'd suggest reading this article for background: Java Garbage Collection.
What Is a Weak Reference?
By default, all objects are created with strong references. As long as an object has a strong reference pointing to it, it’s assumed to be in use, and it won’t be garbage collected.
Weak references, created using the WeakReference() class, won’t stop an object from being garbage collected. If the weak reference is the only pointer to the object, it can be garbage collected at any time.
Typically, they could be used for an object that’s passed to a background process, such as an image renderer, and won’t be needed once that process is complete. The background process holds a strong reference to the object, but when it completes, the object can be garbage collected as only the weak reference remains.
They’re useful for preventing memory leaks, but as we’ll see, they may not work as they should if the object they refer to overrides the default finalize()
method.
How Is the finalize() Method Handled by the Java Garbage Collector?
The finalize()
method is executed just before an object is destroyed by the garbage collector, and it aims to give programmers the chance to carry out clean-up tasks.
However, since Java 9, it has been deprecated, because it doesn’t always work as it should.
The garbage collector works as follows:
- If an object has no strong references pointing to it, it’s marked as finalizable;
- The garbage collector calls the object’s
finalize()
method; - When this completes, the object is destroyed.
The garbage collector calls the finalize()
method of each eligible object in turn: the methods are queued sequentially. This can go wrong in two ways:
- The
finalize()
method may wait for resources, and take a long time to complete. This holds up all the other objects in the queue, so none of the memory they're holding can be released. - It’s not guaranteed the method will ever be executed: it may still be sitting in the queue when the program completes. If the method contained important tasks, such as committing data, this may cause problems.
When It All Goes Wrong
Let’s look at a program that deliberately causes a memory leak within the finalize()
method.
First, we have a class named BuggyClass
. It uses a significant amount of memory, and contains a bug: an infinite loop in its finalize()
method. The finalize()
method prints a message so we can see when it runs.
This simulates what can happen if a finalize()
method hangs while waiting for a resource.
public class BuggyClass {
int[] a = new int[2000000];
public BuggyClass() {
}
public void finalize() {
System.out.println("Finalizing");
while(true)
a[1]=100;
}
}
TestClass creates six BuggyClass
objects using weak references. It contains a delay to give time for garbage collection to occur, and for us to capture diagnostics.
import java.lang.ref.WeakReference;
public class TestClass {
public TestClass() {
WeakReference<BuggyClass> a=null;
WeakReference<BuggyClass> b=null;
WeakReference<BuggyClass> c=null;
WeakReference<BuggyClass> d=null;
WeakReference<BuggyClass> e=null;
WeakReference<BuggyClass> f=null;
methodA(a);
methodA(b);
methodA(c);
methodA(d);
methodA(e);
methodA(f);
try
{Thread.sleep(150000);}
catch (Exception ex){}
}
public void methodA(WeakReference a) {
a = new WeakReference<BuggyClass>(new BuggyClass());
}
}
Theoretically, when methodA()
completes, object a
should be garbage collected, since only a weak reference remains. However, due to the bug in BuggyClass
, the garbage collector won’t be able to reclaim the memory. This is because the finalize()
method of the first object never completes.
Lastly, the main program creates an object from TestClass
.
public class BuggyProg {
public static void main(String[] args) {
TestClass a = new TestClass();
}
}
Running the Program: Results and Diagnostics
When the program is run, we see:
C:\javadev\articles\demos>java BuggyProg
Finalizing
C:\javadev\articles\demos>
Only one of the objects had its finalize()
method invoked: the others are still waiting in the queue. Although this method can never end ordinarily, it was terminated regardless when the main program completed. The finalize()
method, therefore, can’t be relied on for important tasks.
Now let’s see what’s happening in memory by taking a heap dump using the jcmd utility. The easiest way to analyze this is to use HeapHero, which provides diagnostics and recommendations from the contents of the heap.
Below are sections of the HeapHero report, illustrating that memory is not being freed when methodA()
completes, in spite of the weak references.
All six instances of BuggyClass
are still retained in memory:
Conclusion
We’ve looked at one example where garbage collection may not behave as expected. Relying on weak references to improve memory usage only works when combined with good programming practices.
The finalize()
method can be dangerous, and it’s quite rightly deprecated in later versions of Java.
Opinions expressed by DZone contributors are their own.
Comments