Back to JVM Tuning & Troubleshooting
Q8Question 8 of 15

How do you detect a memory leak?

Key Concepts
  • Monitor heap usage over time – steady growth without plateau = leak
  • Enable GC logging: -Xlog:gc* (Java 9+)
  • Capture heap dump: jmap or -XX:+HeapDumpOnOutOfMemoryError
  • Analyze with Eclipse MAT: Leak Suspect Report + Dominator Tree
  • Common leaks: static collections, unclosed resources, ThreadLocal in pools

Definition

A leak is useless objects still referenced; you spot it by ever-rising heap and ineffective Full GCs, then confirm it with a heap dump analyzed in Eclipse MAT.

A Java memory leak happens when objects that are no longer needed remain reachable through some reference chain, so the garbage collector cannot reclaim them. The classic signature is heap usage that trends upward over time, Full GCs that run more and more frequently yet free less and less memory, and eventually an OutOfMemoryError. You detect it by watching the sawtooth GC pattern drift upward, capturing a heap dump (automatically with -XX:+HeapDumpOnOutOfMemoryError), and analyzing the dump in a tool like Eclipse MAT or VisualVM to find which objects dominate the heap and what is keeping them alive.

Understanding the Concepts

What does a memory leak look like?

Healthy heap usage looks like a sawtooth: it rises as objects are allocated, then drops sharply after each GC as garbage is reclaimed, staying within a stable band. A leak breaks this pattern. The low points of the sawtooth creep steadily upward over hours or days because each GC leaves behind more objects than the last, since those objects are still reachable. This upward drift of the post-GC baseline is the single clearest sign of a leak.

DIAGRAM
  Healthy (sawtooth stays flat):     Leak (baseline drifts upward):
  ╱╲  ╱╲  ╱╲  ╱╲  ╱╲  ╱╲          ╱╲  ╱╲   ╱╲    ╱╲     ╱╲
 ╱  ╲╱  ╲╱  ╲╱  ╲╱  ╲╱  ╲        ╱  ╲╱  ╲ ╱  ╲  ╱  ╲   ╱  ╲
 ────────────────────────          ╱    ╲╱   ╲╱    ╲╱    ╲ ╱    OOM!
  post-GC baseline is flat          post-GC baseline climbs

What happens as the leak progresses?

As the leak grows, the Old Generation fills with live-but-useless objects. The JVM responds by running Full GCs more and more often, trying to reclaim space, but because the objects are still referenced, each Full GC frees almost nothing. You see Full GCs clustering together, the application pausing frequently, and GC time rising as a percentage of uptime. Finally the heap cannot satisfy an allocation and the JVM throws OutOfMemoryError: Java heap space.

How do you capture a heap dump?

To diagnose, enable -XX:+HeapDumpOnOutOfMemoryError so that when the OOM happens the JVM automatically writes a .hprof snapshot. You can also trigger a dump on demand.

BASH
# Automatic dump on OOM (set at JVM startup)
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/dumps/bank-api.hprof \
     -Xmx4g -jar bank-api.jar

# Manual dump from a running process
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>

# Modern alternative
jcmd <pid> GC.heap_dump /tmp/heap.hprof

How do you analyze the dump?

Open the dump in Eclipse Memory Analyzer (MAT). MAT's Dominator Tree shows which objects retain the most memory, and its "Leak Suspects" report often points straight at the culprit. The key concept is the retention chain: MAT shows the "path to GC roots," the chain of references keeping an object alive, which tells you exactly what to fix.

What are common leak patterns in banking code?

JAVA
package com.bank.service;

import java.util.HashMap;
import java.util.Map;

public class AuditCache {
    // LEAK: static map that only grows – nothing ever removes entries
    private static final Map<String, String> auditLog = new HashMap<>();

    public void recordAudit(String txnId, String detail) {
        auditLog.put(txnId, detail);  // grows without bound!
    }
}

// FIX: use a bounded cache with eviction
// Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(Duration.ofHours(1)).build();
  • Other common leak causes:
  • Listeners or callbacks registered but never unregistered. Each registration adds an object to a list that grows forever.
  • ThreadLocal values not removed in a thread pool. The pool reuses threads, so the ThreadLocal value persists across requests.
  • Unclosed resources (streams, connections) that hold native memory or buffer references.

How do you catch leaks before an OOM?

A senior engineer does not wait for a crash. You trend heap usage after GC on a monitoring dashboard and alert when the post-GC baseline climbs steadily over time.

BASH
# Watch post-GC heap usage with jstat
jstat -gcutil <pid> 5000
# Look at the O (Old Gen usage %) column – if it climbs to 90%+ and stays, investigate

In banking applications at National Bank of India, Rajesh sets an alert when the post-GC Old Gen usage exceeds 80% for more than 10 minutes, giving the team time to capture a heap dump and investigate before the service crashes.

Key Takeaway

A memory leak in Java is not about memory that the language forgot to free. It is about objects that are no longer needed but are still reachable through some reference chain, so the garbage collector is not allowed to reclaim them. The classic signature is unmistakable. Healthy heap usage looks like a sawtooth that rises and then drops back after every collection, staying inside a stable band. A leak makes the low points of that sawtooth creep steadily upward over hours or days, because each collection leaves behind more live-but-useless objects than the last. As the Old Generation fills, the JVM runs Full GCs more and more often but frees less and less, until finally it cannot satisfy an allocation and throws OutOfMemoryError.

To diagnose it, I always run production with -XX:+HeapDumpOnOutOfMemoryError and a -XX:HeapDumpPath set, so that the instant an OOM happens the JVM automatically writes a full heap snapshot to a .hprof file instead of losing the evidence. I can also take a dump on demand from a running process with jmap or JFR. Then I open that dump in Eclipse Memory Analyzer, where the Dominator Tree shows me which objects retain the most memory and the path to GC roots shows exactly what reference is keeping them alive. The usual culprits are a static collection that only grows, listeners that are registered but never removed, or ThreadLocal values left behind in a thread pool.

In my banking work at National Bank of India, I hit this exact problem. We had added a static map that cached every transfer's audit record for quick lookups, but nothing ever evicted old entries, so it grew without bound. Operators saw the service pausing more and more as the week went on, and the GC logs showed Full GCs clustering while freeing almost nothing, which is the textbook leak signature. After the heap dump, Eclipse MAT pointed straight at that static HashMap retaining most of the heap, and the fix was to replace it with a bounded cache that evicts by size and time-to-live. The lasting lesson I took away is to trend the post-GC heap baseline on a dashboard and alert when it climbs steadily, so we catch the leak early rather than waiting for a crash.

Remember one sentence: How to detect a memory leak: heap usage keeps growing, Full GC runs more frequently but cannot free memory, eventually OutOfMemoryError: tools: heap dump (-XX:+HeapDumpOnOutOfMemoryError), analyze with Eclipse MAT or VisualVM is fundamental knowledge for Java interviews and production banking work.

---

Share
Use ← → arrow keys to navigate