Alright, let’s get our hands dirty. You’ve got an application that’s slowly turning into a digital beached whale, consuming memory until it gasps its last breath and gets unceremoniously killed by the operating system. You, my friend, have a memory leak. It’s not a question of if you’ll face one, but when. Diagnosing them feels like detective work, and I’m here to give you your magnifying glass and trench coat.

The core principle is simple: memory is allocated (e.g., with new, malloc, or by adding elements to a collection) but is never reclaimed by the garbage collector (GC) because something, somewhere, still holds a reference to it. Your job is to find that illegitimate reference.

The First Clue: Are You Even Leaking?

Before you start tearing your hair out, confirm you’re actually dealing with a leak and not just a healthy application that uses a lot of memory. A leak has a specific signature: a steady, never-ending upwards climb in heap usage over time, especially after the main operations of your app have completed.

The easiest way to spot this is by using a visual tool. For the JVM, JConsole or the more advanced VisualVM (or its modern successor, JDK Mission Control) are your best friends. Fire one up, connect to your running process, and watch the heap graph. Perform a key operation—say, loading a large report—and then let the app sit idle. A healthy app will see a sharp spike in memory use, followed by a sawtooth pattern as the GC does its job. A leaking app will see that spike but the baseline of the sawtooth will creep ever upward, never settling back down.

For a quick and dirty command-line option, you can force a garbage collection and get a heap summary. This is a diagnostic trick, not a solution—never use System.gc() in production code. But for testing, it can reveal a lot.

// A quick test to see baseline memory after a GC
public class MemoryLeakSuspect {
    public static void main(String[] args) {
        Runtime rt = Runtime.getRuntime();
        long preGc = rt.totalMemory() - rt.freeMemory();
        System.gc(); // Seriously, just for testing.
        Thread.sleep(1000); // Give the GC a moment to finish
        long postGc = rt.totalMemory() - rt.freeMemory();
        System.out.println("Pre-GC: " + preGc / (1024 * 1024) + " MB");
        System.out.println("Post-GC: " + postGc / (1024 * 1024) + " MB");
    }
}

Run this before and after your suspect operation. If postGc is significantly higher and that memory isn’t reclaimed after subsequent operations, you’ve got a leak.

Getting a Heap Dump: The Crime Scene Photograph

Once you’ve confirmed the leak, you need a heap dump. This is a snapshot of every object in memory and, crucially, what references it. It’s your single most powerful tool.

You can trigger one in several ways:

  1. Using jmap: jmap -dump:live,file=heapdump.hprof <your_pid>
  2. Using JVM args: Add -XX:+HeapDumpOnOutOfMemoryError to your startup command. This is non-negotiable for production systems; it will automatically dump the heap when it crashes, giving you a perfect picture of the cause.
  3. From VisualVM/JConsole: Just click a button.

Now, analyze that .hprof file. This is where Eclipse MAT (Memory Analyzer Tool) shines. It’s brilliant. Load the dump and let it work its magic. It will often give you a “Leak Suspects Report” that points directly to the culprit.

The Usual Susects: Where to Point Your Finger

MAT will show you the biggest retained heaps—the chunks of memory that are being kept alive. Nine times out of ten, the leak is in one of these classic patterns:

  1. Static Fields: This is the king of leaks. A static Map that caches user sessions but never evicts them? That map lives for the life of the classloader (usually the entire application). Everything in it is effectively immortal.
// A classic, terrible idea
public class SessionManager {
    private static final Map<String, UserSession> SESSION_CACHE = new HashMap<>();

    public static void addSession(String key, UserSession session) {
        SESSION_CACHE.put(key, session); // Forever. And ever.
    }
    // ... but where is the remove?!
}
  1. Unclosed Resources: Anything that implements CloseableInputStream, Connection, Statement, Socket. Not closing these is a double-whammy: you leak memory and some other OS-level resource (like file handles). Use try-with-resources. Always. No excuses.
// Bad. Don't do this.
FileInputStream fis = new FileInputStream("huge_file.txt");
// ... read stuff
// fis is never closed! The byte[] buffer inside it never gets GC'd.

// Good. The compiler writes the finally block for you.
try (FileInputStream fis = new FileInputStream("huge_file.txt")) {
    // ... read stuff
} // fis.close() is called here, even on exceptions.
  1. Listeners and Callbacks: You add a listener to a central event bus but forget to remove it when the object is no longer needed. The event bus, being a long-lived citizen, holds a reference to your object, keeping it alive indefinitely.
  2. Internal Data Structures: Some classes have… surprising choices. The classic example is String.substring() in older Java versions. It used to share the original char array, meaning a tiny substring could accidentally hold a reference to a giant original string. This was a design flaw so infamous they actually changed it in later updates.

Advanced Sleuthing: Comparing Heap Dumps

Sometimes the leak is subtle. The best technique here is to take two heap dumps a few minutes apart while the leak is progressing. Load both into MAT and use its “Compare Tables” feature. This will show you which object types are growing in number between the two dumps. Seeing ten million MyCustomEventObject instances when you should only have a few hundred is a dead giveaway. Now you can trace the dominator tree for that specific class to find what’s holding onto all of them.

The key is a methodical process: confirm the leak, get a dump, analyze the biggest retained sets, and look for these common anti-patterns. It’s frustrating, but finding and fixing a gnarly leak is one of the most satisfying feelings in this job. You’ve slain a silent, invisible beast. Now go get it.