Skip to content
  • References & Memory Leaks · Java

References & Memory Leaks

LearnViaNotes @learnvianotes· 4 min read

Java has garbage collection, not leak immunity: anything you keep reachable by accident is a leak.

Four reference strengths

TypeCollected when…get() after collectionTypical use
Strong (Obj o = …)Never, while reachable—Normal code
Soft — SoftReferenceOnly when memory is low (before OOM)nullMemory-sensitive caches
Weak — WeakReferenceAt the next GC once only weakly reachablenullWeakHashMap, canonicalising maps, listener registries
Phantom — PhantomReferenceAfter finalisation; enqueued for cleanupalways nullPost-mortem cleanup (Cleaner is built on it)
Java
import java.lang.ref.WeakReference;

public class Main {
    public static void main(String[] args) {
        StringBuilder data = new StringBuilder("payload");
        WeakReference<StringBuilder> weak = new WeakReference<>(data);

        System.out.println(weak.get());          // still strongly reachable via `data`
        data = null;                             // drop the only strong reference
        System.gc();                             // a hint — usually honoured in practice
        System.out.println(weak.get());
    }
}
Typical output
payload
null

WeakHashMap

Keys are held weakly: when a key has no strong references elsewhere, its entry disappears on a later GC. Useful for attaching metadata to objects you don't own.

The usual leak suspects

LeakWhy it's reachableFix
Static collection used as a cacheStatic fields are GC roots — the map lives as long as the classBounded cache (LRU via LinkedHashMap.removeEldestEntry, Caffeine), or eviction
Listeners / callbacks never removedThe subject's listener list points at every subscriberUnregister in a close(); or weak listeners
Non-static inner class / lambda capturesAn inner instance holds Outer.this; lambdas hold captured variablesUse static nested classes; capture only what you need
ThreadLocal in thread poolsPool threads never die, so their thread-local values never go awaytry { … } finally { tl.remove(); }
Unclosed resourcesStreams, connections, native buffers hold memory and file handlestry-with-resources
Wrong equals/hashCode as map keyDuplicate keys pile up; mutated keys become unreachable-yet-presentImmutable keys with a correct contract
Java
import java.util.ArrayList;
import java.util.List;

public class Main {
    static final List<Runnable> LISTENERS = new ArrayList<>();   // static → a GC root

    public static void main(String[] args) {
        for (int i = 0; i < 1_000; i++) {
            byte[] buffer = new byte[10_000];
            LISTENERS.add(() -> System.out.print(buffer.length));   // lambda captures buffer
        }
        // ~10 MB can never be freed: every buffer is reachable through LISTENERS
        System.out.println("listeners: " + LISTENERS.size());

        LISTENERS.clear();                 // unsubscribe → buffers become garbage
        System.out.println("listeners: " + LISTENERS.size());
    }
}
Output
listeners: 1000
listeners: 0
Java
public class Main {
    private static final ThreadLocal<StringBuilder> BUFFER =
            ThreadLocal.withInitial(StringBuilder::new);

    static String handle(String request) {
        try {
            StringBuilder sb = BUFFER.get();
            sb.setLength(0);
            return sb.append("handled ").append(request).toString();
        } finally {
            BUFFER.remove();          // pooled threads live forever — always clean up
        }
    }

    public static void main(String[] args) {
        System.out.println(handle("GET /"));
    }
}
Output
handled GET /

Finding a leak

  1. Confirm

    Heap usage after each full GC keeps climbing (GC logs, JFR, VisualVM / JConsole graphs).

  2. Dump the heap

    jcmd <pid> GC.heap_dump heap.hprof (or jmap -dump:live,format=b,file=heap.hprof <pid>, or -XX:+HeapDumpOnOutOfMemoryError).

  3. Analyse

    Open it in Eclipse MAT or VisualVM: look at the dominator tree and the path to GC roots of the biggest retained sets.

  4. Fix & verify

    Remove the unintended reference, then re-run and compare dumps.

Terminal
$ jcmd                                   # list running JVMs
$ jcmd <pid> GC.class_histogram | head   # which classes use the most memory
$ jcmd <pid> GC.heap_dump /tmp/heap.hprof
$ jstat -gcutil <pid> 1000               # GC utilisation every second

How can a garbage-collected language leak memory?

SoftReference vs WeakReference?

Why can a non-static inner class cause a leak?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report