Java Memory Leak: Stop the Blame Game (How to Prove a Memory Leak is (or Isn’t) a Code Issue)

This is a Java team’s story. If production goes down with an OutOfMemoryError, before the heap dump has even flushed to disk, blame is being thrown. The dev team believes that the server needs to be expanded. The ops team believes that someone has deployed bad code. Then the vendor receives a brief e-mail at 2 a.m.

It’s not about whether they disagree or not, it’s that no one has produced evidence. This article is a solution to that. We’ll go through a systematic and data-informed process to uncover if a memory problem in Java is a coding problem or an infrastructure, configuration, or operational problem. These tools are heap dumps, GC logs and a bit of discipline.

Java Memory Leak Detection: Is It the Code or the Infrastructure? 

Memory leaks do not suddenly bring programs to a halt. They trick them into dying a slow death. Heap grows over hours (and sometimes days), and when an OutOfMemoryError occurs, it’s trapped in history without most teams knowing. On executing the program, the application crashes with an OutOfMemoryError. To understand exactly what your crash means, check out this guide on the different types of OutOfMemoryError.

Fig: Application crashing with OutOfMemoryError

Why Java Memory Leak Detection Often Turns Into a Blame Game 

In case of a crash after a release, the developers receive the call. As CPU spiking, operations is in the hot seat. If it is a third-party dependency in the stack trace, the vendor receives a ticket. Both sides have circumstantial evidence and a story; however, no one has been able to identify any object retention data and/or any GC recovery patterns. Thus, the argument goes around and around.

The gap most teams are diagnosed with:

  • No data to analyse because the trend data is not stored in GC logs.
  • No heap dump capture on failure is enabled, which means that the crash evidence is lost.
  • No information on which objects are accumulating on dashboards is provided with heap usage totals.

By the end of the day teams squabble over graphs instead of data. The important 3 questions remain unanswered: Is the heap getting larger because something is leaking, because there is more traffic, or because the JVM is misconfigured? The instruments to answer these questions are not turned on.

The Cost of Poor Java Memory Leak Detection 

Outside of the incident: Speculative rollbacks, vendor diagnostic requests, and emergency patching of the wrong component. Both of these carry an engineering cost and organisational goodwill. The cure is always the same, gather evidence prior to concluding not after.

What Is a Java Memory Leak? Understanding the Basics Before Detection 

Being specific here isn’t doing it justice, as it is misused all the time. Many ‘memory leak’ tickets are just the normal JVM behaviour when loaded. There are some heaps that aren’t a leak, and to treat them all like a leak is a waste of time. In fact, a memory leak in Java happens when the memory is retained and not released. See Figure 2 for an explanation of a Java memory leak. Explore our comprehensive guide on Java memory leaks to have a deeper understanding of how these memory leaks occur.

Fig: Java memory leak explained

How Object Retention Impacts Java Memory Leak Detection 

The Garbage Collector of the JVM works as expected. It retrieves all the objects without any live references. The problem is that this is a logic problem, not a GC failure – and a “leak” in Java is a logic problem. The application holds a reference to an object not used by it anymore. The application possesses a reference to an object that is no longer used by it, and this reference is still valid, so the GC can’t get its hands on it. These objects accumulate in this heap until the JVM runs out of space.

Most typical causes:

  • Static collections that nobody is ever clearing
  • Caches with no eviction policy
  • Event listeners that get registered but never removed
  • ThreadLocal variables left populated after a request completes

The JVM is running as per its specification. The objects are continuing to live, as instructed by the code. This is the difference between it being an infrastructure issue or a coding issue.

Java Memory Leak Detection: Distinguishing Traffic Spikes from Memory Leaks 

The most popular misdiagnosis is that of traffic spike and memory leak. They both cause the heap to grow, and they look alike on a heap chart. The difference is that when a load drops, certain things happen. Before opening the first heap dump, use this table to distinguish them. 

SymptomTraffic SpikeMemory Leak
Heap usage patternSpikes under load, drops back cleanlyClimbs steadily — does not recover after load drops
Post-GC baselineReturns near-normal after each Full GCRises with every successive Full GC cycle
CPU behaviourHigh CPU tracks directly with traffic volumeGC CPU spikes grow even as traffic stays flat
Response timeDegrades with load, recovers when load easesDegrades progressively under constant load
Object countGrows under load, drops proportionallySpecific class counts climb even with flat traffic
Memory after load stopReturns to idle baseline quicklyStays elevated — objects outlive their scope
GC frequencyRises with load, normalises when traffic fallsIncreases over time regardless of traffic level
Likely culpritInfrastructure or heap sizingApplication code — object retention bug

The single best signal is the post-GC heap baseline. If it increases with every Full GC cycle, regardless of traffic, then there’s something it’s retaining memory it shouldn’t be retaining. It’s a code issue.

Prepare for Java Memory Leak Detection Before the Next Crash 

Most post-mortems don’t fail because the team is not skilled; they fail because the JVM configuration was not properly set up when the crash occurred. Now add the four arguments to your application’s startup script, BEFORE the next incident:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump.hprof
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20m
-Xmx4g # in production always specify max heap

  1. -XX:+HeapDumpOnOutOfMemoryError: Dumps a complete dump of the heap on JVM out of memory error. If not, the evidence of the crash is lost.
  2. -Xlog:gc*(for Java9 and above): Prints GC telemetry in detail — the baseline trend after the GC, which indicates if the heap is truly leaking or if traffic is putting pressure on the heap. The rotating file config keeps the size of log files manageable.
  3. -Xmx: Allows the heap to be limited to a certain maximum size. When there is no definite limit, the JVM will use up its resources, finally crashing and this OutOfMemoryError will be hard to pinpoint the exact problem.

With no heap dump file and no GC log, no diagnosis. It’s that simple.

Java Memory Leak Detection: How to Prove It’s a Code Issue 

Once you have the GC logs and a heap dump, the answer to this question becomes straightforward: “Is this application leaking memory?” Here are the four signals that answer the question.

1. Post-GC Heap Size Continues to Increase 

The collector removes all the unreachable objects after each Full GC. The remaining memory is the amount of memory that the application actually needs at that time, and only that. If that floor is increasing during each cycle of GC, it means that something is being maintained that shouldn’t be. The heap usage is increasing in the subsequent Full GC cycles, as shown in Fig. 3.

Fig: The amount of memory used by the heap is growing over subsequent Full GC cycles

GC is working. It simply can’t release objects that the code is still using. This is the job of the application rather than the JVM.

Similar GC-driven troubleshooting examples: GC Overhead Limit Exceeded: How to Troubleshoot?

2. Object Counts Keep Growing in a Single Class 

If you compare two heaps taken under identical conditions (same load, etc.), it will almost always reveal it: a single class where the number of instances is increasing at a steady rate. Common examples:

  • Well over the number of user sessions
  • Instances of HashMap growing without any corresponding eviction.Instances where HashMap instances are increasing without any eviction.
  • Too many event listener objects without deregistration

When you identify the class, open HeapHero and go to the Dominator Tree. This is a view of the objects sorted by Retained Size, the amount of memory that would be released if this object were to be collected, including memory that is transitively held by it. If a class has a bigger percentage of the retained heap for two dumps in a row, then that class is the one you have a problem with. This is partly due to the fact that retained size is more meaningful than just the number of objects, since it gives an idea of the cost of keeping a single object alive downstream.

3. Static Objects Retain Request-Scoped Data 

Static fields are objects with a lifetime equal to that of the JVM. If there’s a collection with request-scoped and session-scoped objects, then those objects are now permanent. They are never to be touched by the GC.

public class SessionCache {
public static List<UserSession> sessions = new ArrayList<>();
// All sessions added here are persistent and will remain until JVM ends.
// No eviction = memory growth is guaranteed when sustaining load.
}

It is not necessary to periodically delete the list. The issue is that if an object has a short lifecycle, it should never be stored in a reference with an application-wide lifetime.

4. Memory Does Not Recover After Load Drops 

There is no tooling required for this test. Add a load cycle, then take it off completely and observe the heap:

  • Load on → heap increases. Expected.
  • Load off: heap should drop towards baseline. If not, then something is holding stuff in its hands after it’s out of scope.
  • Second load cycle → if the heap peaks higher than the first cycle, object retention is confirmed.

This is one point that clears out the vendor and the infrastructure team. If the heap is undersized, the pressure should be the same throughout, rather than increasing with load cycles. If the memory doesn’t free when the work stops, it is not a work problem; it is a problem with the code used to do the work.

Conclusion

Blame games for Java Memory Incidents occur for this reason: No Evidence. Once you’re able to get your GC logs, heap dump, and a sane method to read them, it’s a different story. The data indicates where the code is, or it does not; there’s no room for guessing.

Here are 4 key takeaways from the article:

  •  -XX:+HeapDumpOnOutOfMemoryError option helps to ensure that a dump is created whenever a memory failure occurs; Enable GC logging – All these preparation are required in production phase
  •  Post analysis of GC heap clearly distinguishes Traffic pikes and memory leaks.
  •  Memory can be used by multiple classes; tools like HeapHero provide features like Dominator Tree and Retained Size – to identify the exact class which has maximum memory usage
  •  If memory usage does not get reduced when load decreases, then it is a code problem.

There is no better memory analysis than the analysis of remembered object trends, GC recovery patterns and repeatable evidence. All others are noise. For advanced debugging examples, check How to Analyze Java Heap Dumps Effectively Using Tools.

Share your Thoughts!

Up ↑

Index

Discover more from HeapHero – Java & Android Heap Dump Analyzer

Subscribe now to keep reading and get access to the full archive.

Continue reading