Memory Leaks, Be Gone!

by Staffan Larsen
06/27/2005

Abstract

Although the Java Virtual Machine (JVM) and its garbage collector (GC) manage most memory chores, it is possible to have memory leaks in Java software programs. Indeed, this is quite a common problem in large projects.  The first step to avoiding memory leaks is to understand how they occur. This article presents some common pitfalls  and best practices for writing non-leaking Java code. Once you have a memory leak, it can be very difficult to pinpoint the code that creates the leak. This article also presents a new tool for efficiently diagnosing the leak and pinpointing the root cause. The tool has a very low overhead, allowing you to find memory leaks in production-type systems.

The Role of the Garbage Collector

While the garbage collector takes care of most of the problems involving management of memory, making life easier for the programmer, it is possible for the programmer to make mistakes that lead to memory issues. Stated simply, the GC works by recursively following all references from the “root” objects (objects on the stack, static fields, JNI handles, and so on) and marking as live all the objects it can reach. These become the only objects the program can ever manipulate; any other objects are deleted. Since the GC makes it impossible for the program to reach the objects that are deleted, it is safe to do so.

Although memory management might be said to be automatic, it does not free the programmer from thinking about memory management issues. For example, there will always be a cost associated with allocating (and freeing) memory, although this cost is not made explicit to the programmer. A program that creates too many objects will be slower than one that does the same thing with fewer objects (provided all other things are equal).

And, more relevant for this article, it is possible to create a memory leak by forgetting to “free” memory that has previously been allocated. If the program keeps references to objects that will never be used, the objects will hang around and eat up memory, because there is no way for the automatic garbage collector to prove that these objects will not be used. As we saw earlier, if a reference to an object exists, it is by definition live and therefore is not deleted. To make sure the object’s memory is reclaimed, the programmer has to make sure it is no longer possible to reach the object. This is typically done by setting object fields to null or removing objects from collections. Note, however, that it is not necessary to explicitly set local variables to null when they are no longer used. These references will be cleared automatically as soon as the method exits.

On a high level this is what all memory leaks in memory-managed languages revolve around; leftover references to objects that will never be used again.

Typical Leaks

Now that we know it is indeed possible to create memory leaks in Java, let’s have a look at some typical leaks and what causes them.

Global collections

It is quite common in larger applications to have some kind of global data repository, a JNDI-tree for example, or a session table. In these cases care has to be taken to manage the size of the repository. There has to be some mechanism in place to remove data that is no longer needed from the repository.

This may take many different forms, but one of the most common is some kind of cleanup-task that is run periodically. This task will validate the data in the repository and remove everything that is no longer needed.

Another way to manage this is to use reference counting. The collection is then responsible for keeping track of the number of referrers for each entry in the collection. This requires referrers to tell the collection when they are done with an entry. When the number of referrers reaches zero, the element can be removed from the collection.

Caches

A cache is a data structure used for fast lookup of results for already-executed operations. Therefore, if an operation is slow to execute, you can cache the result of the operation for common input data and use that cached data the next time the operation is invoked.

Caches typically are implemented in a dynamic fashion, where new results are added to the cache as they are executed. A typical algorithm looks like:

  1. Check if the result is in the cache, if so return it.
  2. If the result is not in the cache, calculate it.
  3. Add the newly calculated result to the cache to make it available for future calls to the operation.

The problem (or potential memory leak) with this algorithm is in the last step. If the operation is called with a very large number of different inputs, a large number of results will be stored in the cache. Clearly this isn't the correct way to do it.

To prevent this potentially fatal design, the program has to make sure the cache has an upper bound on the amount of memory it will use. Therefore, a better algorithm is:

  1. Check if the result is in the cache, if so return it.
  2. If the result is not in the cache, calculate it.
  3. If the cache is too large, remove the oldest result from the cache.
  4. Add the newly calculated result to the cache to make it available for future calls to the operation.

By always removing the oldest result from the cache we are making the assumption that, in the future, the last input data is more likely to recur than the oldest data. This generally is a good assumption.

This new algorithm will make sure the cache is held within predefined memory bounds. The exact bounds can be difficult to compute since the objects in the cache are kept alive as is everything they reference. The correct sizing of the cache is a complex task in which you need to balance the amount of used memory against the benefit of retrieving data quickly.

Another approach to solving this problem is to use the java.lang.ref.SoftReference class to hold on to the objects in the cache. This guarantees that these references will be removed if the virtual machine is running out of memory and needs more heap.

ClassLoaders

The use of the Java ClassLoader construct is riddled with chances for memory leaks. What makes ClassLoaders so difficult from a memory-leak perspective is the complicated nature of the construct. ClassLoaders are different in that they are not just involved with “normal” object references, but are also meta-object references such as fields, methods, and classes. This means that as long as there are references to fields, methods, classes, or objects of a ClassLoader, the ClassLoader will stay in the JVM. Since the ClassLoader itself can hold on to a lot of classes as well as all their static fields, quite a bit of memory can be leaked. 

Spotting the Leak

Often your first indication of a memory leak occurs after the fact; you get an OutOfMemoryError in your application. This typically happens in the production environment where you least want it to happen, with the possibilities for debugging being minimal. It may be that your test environment does not exercise the application in quite the same way the production system does, resulting in the leak only showing up in production. In this case you need some very low-overhead tools to monitor and search for the memory leak. You also need to be able to attach the tools to the running system without having to restart it or instrument your code. Perhaps most importantly, when you are done analyzing, you need to be able to disconnect the tools and leave the system undisturbed.

While an OutOfMemoryError is often a sign of a memory leak, it is possible that the application really is using that much memory; in that case you either have to increase the amount of heap available to the JVM or change your application in some way to use less memory. However, in a lot of cases, an OutOfMemoryError is a sign of a memory leak. One way to find out is to continuously monitor the GC activity to spot if the memory usage increases over time. If it does, you probably have a memory leak.

Verbose output

There are many ways to monitor the activity of the garbage collector. Probably the most widely used is to start the JVM with the -Xverbose:gc option and watch the output for a while.

[memory ] 10.109-10.235: GC 65536K->16788K (65536K), 126.000 ms

The value after the arrow (in this case 16788K) is the heap usage after the garbage collection.

Console

Looking at endless printouts of verbose GC statistics is tedious at best. Fortunately there are better tools for this. The JRockit Management Console can display a graph of the heap usage. With this graph it is easy to see if the heap usage is growing over time.

Figure 1
Figure 1. The JRockit Management Console

The management console can even be configured to send you an email if the heap usage is looking bad (or on a number of other events). This obviously makes watching for memory leaks much easier.

Pages: 1, 2

Next Page »