Java

Troubleshooting Java Memory Issues

Last updated Jul 29, 2005.

I have talked in some length about memory and memory issues, but rarely in one place. Here, I look at some specific memory issues, how to identify them, and —the important part— how to resolve them.

Java provides robust memory management through advanced garbage collection algorithms, but even the best memory management algorithm cannot avoid human error.

Back in the days of C/C++ programming, memory management was in the hands of programmers, who became very cognizant of the implications of not properly deallocating memory. If memory was not properly deallocated, it was lost for the duration of the application, and an inappropriate pointer could lead to an operating system crash.

Java, therefore, created the notion of a virtual machine, and a "sandbox" in which its applications would run. Because Java manages the memory, it can assure that memory requests stay within the virtual machine, and when objects are no longer reference-able it can automatically reclaim that memory. The end result is that operating systems are safer from rogue applications, and C/C++ memory leaks are avoided. The byproduct, however, is that the modern Java developers who do not come from a programming background requiring strict memory management are less cognizant of memory issues.

While C/C++ style memory leaks are avoided in Java Virtual Machines, there are two problems that commonly occur in Java applications:

Every time a garbage collection occurs, the garbage collector must determine what objects are valid (or currently referenced) and free those objects. The process is referred to as mark and sweep: the garbage collector traverses the heap, and marks objects that are currently in use and then sweeps away the dead objects. Some garbage collection algorithms follow this phase with a "compact" phase that compresses the heap memory for best performance and future allocation.

This points to two inherent limitations to this procedure:

We refer to the first problem as object-cycling, and the second as lingering references.

Object Cycling

Object cycling can be detected in the heap either through a visual tool that samples memory very frequently (once a second or less) — you will observe very narrow choppy marks in the heap — or through garbage collection logs. The key indicator to look for in reading garbage collection logs is frequent minor collections that reclaim small amounts of memory. Consider that the performance impact of garbage collection on your application is the result of two factors:

Object cycling impacts the frequency of garbage collection and can be therefore be detected by calculating the frequency of garbage collection. You can enable verbose garbage collection reporting in your Java Virtual Machine by passing the following command line parameter to the Java Runtime Engine (java.exe on Windows or java on Unix/Linux):

-verbose:gc

Other options may help you identify heap performace at a deeper level, but this option provides enough information to empower you to calculate the frequency and type of garbage collections. For example:

30.971: [GC 11241K->3574K(130176K), 0.0124588 secs]
31.691: [GC 11766K->3936K(130176K), 0.0104734 secs]
32.536: [GC 12128K->4349K(130176K), 0.0073750 secs]
33.009: [GC 12541K->5472K(130176K), 0.0175936 secs]
33.862: [GC 13664K->6202K(130176K), 0.0115339 secs]
35.999: [GC 14394K->7062K(130176K), 0.0151237 secs]

In this scenario, minor garbage collections running every couple seconds are reclaiming relatively small amounts of memory. This indicates that the application may be cycling objects. Now the difficult part is determining what objects are cycling and how to avoid this behavior. Tools such as Quest's JProbe allow you to profile your code while it is running and report back the number of times an object is created. These creation counts can help you quickly identify where problems exist.

Once you identify objects that are cycling, the next step is to determine if those objects do, in fact, need to be created each time or if they can be cached. In a Web application, this may mean storing the value in the Session or Application context (ServletContext is used for holding objects in application scope). For standalone applications, this may mean storing objects in memory variables or in your own caching infrastructure. The core software development concept here is proper object lifecycle management. Defining object life cycles inside your use cases (when an object is created, how long it lives, and where it is destroyed) will help you manage this issue.

Lingering objects

While object cycling involves creating and destroying objects rapidly, lingering objects refers to creating objects and never destroying them. In other words, you leave a lingering reference to that particular object. This happens somewhat frequently when you use Java collections classes. When you are finished using an object in a collection, you must be sure to remove it from the collection, because otherwise it will not be collected.

This problem is compounded by that fact that lingering object do not usually tend to be individual objects, but rather a subgraph of objects. For example, consider leaving a reference to a Car in memory; it is not just a car, but includes an engine object that includes a radiator, alternator, muffler, etc. So while a single reference may not seem significant, the repercussions can be substantial.

The mechanisms to use to determine if you have lingering objects is to visually observe the behavior of your heap looking for an upward trend, or to analyze garbage collection logs.

In visual observation, the heap's natural pattern is peaks and troughs as objects are created and destroyed. Most applications follow a pattern of spending some time (it could be hours) climbing to a critial mass, then oscillating between peaks and troughs such that the troughs remain relative constant. If your application is "leaking memory" and continually holding on to object references, then when your application reaches its critical path the heap will continue increasing until you run out of memory. The key is to watch the troughs, and try to plot a line through them. After reaching what you believe to be the critical mass, if your plotted line is increasing, there are likely references that your application is not properly freeing.

Garbage collection logs can be very helpful here, too. Instead of looking for rapid minor collections, when detecting lingering objects you are looking for frequent occurences of major garbage collections that are, for the most part, ineffectual. The typical pattern is to see major collections occurring more frequently, freeing less memory, and taking longer to execute.

Detecting lingering references is relatively easy. Let your application run. If it runs out of memory then there is a strong possibility that you are not properly managing object references. Identifying the objects that are lingering in memory is a far more difficult task. There are two options: review your entire application, line-by-line, until you find out where the problem is; or use a memory profiling tool. Some tools, such as Quest's Jprobe, allow you to profile the memory usage of your application. Specifically, you perform the following steps:

  1. Start your application inside the memory profiler
  2. Run a garbage collection to clean up memory
  3. Start a use case
  4. Perform your business case
  5. End the use case
  6. Run a subsequent garbage collection to remove all temporary objects from memory
  7. Look at the differences between the heaps to see what objects are left in memory

The information that you acquire by running your application through a memory profile will help you better define your object lifecycles.

Summary

The main point of this article is to promote a stronger awareness of object lifecycles. You need a clear understanding of when objects are created and destroyed; the best way to do that is to move the object lifecycle definitions into your use cases. This ensures that your quality assurance department does not pass your application code if your objects are not properly managed.

In this article, we identified the two primary causes of memory issues in Java applications: object cycling and lingering objects. We reviewed mechanisms that can be employed to identify both of these conditions and discussed methods to resolve them. A strong awareness of object lifecycles as well as a good set of tools can help improve both the performance and the availability of your applications.

I encourage you to reply to this article with discussions about the tools you use so that other readers may get a broader picture of what is available. Cheers!