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 Classes it has loaded. With multiple redeployments, your application may fail with an unexpected permanent generation memory leak exception.

There are many types of OutOfMemoryErrors. 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!

Byte Leak Heap Usage Graph

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.

List Leak Heap Usage Graph

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.

Map Key Leak Heap Usage Graph

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.

Class Leak PermGen Usage Graph

Thanks for reading! I hope you found this informative.




Enjoy Reading This Article?

Here are some more articles you might like to read next: