Understanding Java Memory Leaks
In Java, you might have the impression that you don’t have to think about memory management. This is true for the majority of cases. However, there are limits. If you create too many objects of varying sizes too quickly, the garbage collector (GC) will work harder, leading to a slow application.
Memory can become more fragmented, which in turn forces the garbage collector to compact heap space, leading to long pauses or the dreaded java.lang.OutOfMemoryError
exception. These long pause times are typically triggered when your Java program attempts to allocate a large object, such as an array.
Modern JVMs are very efficient and can deal effectively with rapid small object creation, but if you hit the limits, your application will either crash or become unresponsive.
The concept of a memory leak is simple: you introduce them by maintaining obsolete references to objects. An obsolete reference is simply one that will never be dereferenced again. This is often called a “simple memory leak.”
There are also “true memory leaks.” You introduce these leaks when you create objects that are inaccessible by running code but are still stored in memory.
One famous example of a true leak involves a concoction of a custom class loader, a long-running thread with thread-local variables, preferably within an application container. This scenario can be particularly tricky. This occurs because the ThreadLocal
keeps a reference to the object, which keeps a reference to its Class
, which in turn keeps a reference to its ClassLoader
. The ClassLoader
, in turn, keeps a reference to all the Class
es it has loaded. With multiple redeployments, your application may fail with an unexpected permanent generation memory leak exception.
There are many types of OutOfMemoryError
s. For more detailed descriptions, refer to the Oracle documentation on memory leaks.
In practice, you will most often encounter these three types:
-
java.lang.OutOfMemoryError: Java heap space
- The Java heap space is exhausted.
-
java.lang.OutOfMemoryError: PermGen space
- The Permanent Generation space is full.
-
java.lang.OutOfMemoryError: GC Overhead limit exceeded
- The Garbage Collector is spending too much time collecting with little to no avail.
In this blog post, I’ve decided to demonstrate how easy it is to create memory leaks. These examples can be handy for code interviews or serve as good illustrations of what not to do.
All examples are runnable. You simply need to clone the codingwithpassion/leaks
repository and execute the Gradle script.
Byte Leak
To run this example, type: gradlew runByteTest
This demonstrates a straightforward memory leak using an ArrayList
and byte arrays. The list continuously grows, with each element holding a reference to a one-megabyte byte array. Arrays need to be allocated as contiguous chunks of memory within the heap space. If memory becomes fragmented, the JVM struggles and eventually throws a java.lang.OutOfMemoryError: Java heap space
exception.
The code for this example is available as a Gist: byteleak.js.
As you can see from the graph below, the Garbage Collector didn’t stand a chance. It’s a massacre!
List Leak
To run this example, type: gradlew runListTest
The list leak is similar to the previous example. It creates a list of BigDecimal
objects that are never dereferenced, making it simple and effective. BigDecimal
is chosen because it is heavier than simpler types like Integer
or Float
.
The code for this example is available as a Gist: listleak.js.
As the graph shows, the GC tries very hard to clean the heap but eventually fails.
Map Key Leak
This next leak is a bit more sophisticated, but at its core, it’s no different from the list leak. It demonstrates what happens when your hashCode
implementation is poor. Elements will be added indefinitely, and each reference will remain active.
You can run this example by typing gradlew runMapBadKeyTest
, or you can type gradlew runMapGoodKeyTest
to test it with a correct key implementation.
The code for this example is available as a Gist: mapleak.js.
This time, the GC barely attempts to recover, possibly because StringBuilder
objects with 100,000 elements are significantly heavier than BigDecimal
, giving it no time to act.
Class Leak
The Permanent Generation (PermGen) holds internal representations of Java classes, among other things (like class names, method data, and String literals). The simplest way to introduce a memory leak in this area is to create too many classes. Another more sophisticated example was mentioned earlier in this post as a “true memory leak.”
To run this example, type: gradlew runClassTest
The code for this example is available as a Gist: classleak.js.
As you can see, the situation escalates pretty quickly. Due to this rapid escalation, you might not always get a PermGen
exception; the application might simply crash unexpectedly.
Thanks for reading! I hope you found this informative.
Enjoy Reading This Article?
Here are some more articles you might like to read next: