Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Core Fundamentals

Object-Oriented Programming

  • Classes and Objects
  • Inheritance and Polymorphism
  • Interfaces and Abstract Classes

Collections Framework

  • List, Set, and Map
  • Generics
  • Collections Framework Recap
  • HashMap Deep Dive
  • TreeMap & LinkedHashMap
  • Iterators & Modification Semantics

Exceptions & Best Practices

  • Exception Handling
  • equals() and hashCode()
  • String Manipulation

JVM Internals & Memory Management

  • JVM Memory Areas
  • Garbage Collection Fundamentals
  • Modern Garbage Collectors
  • Memory Leaks in Java
  • GC Tuning & Diagnostics
Chaturmind
← Java Core Fundamentals

Object-Oriented Programming

  • Classes and Objects
  • Inheritance and Polymorphism
  • Interfaces and Abstract Classes

Collections Framework

  • List, Set, and Map
  • Generics
  • Collections Framework Recap
  • HashMap Deep Dive
  • TreeMap & LinkedHashMap
  • Iterators & Modification Semantics

Exceptions & Best Practices

  • Exception Handling
  • equals() and hashCode()
  • String Manipulation

JVM Internals & Memory Management

  • JVM Memory Areas
  • Garbage Collection Fundamentals
  • Modern Garbage Collectors
  • Memory Leaks in Java
  • GC Tuning & Diagnostics
HomeLearnJavaJava Core FundamentalsJVM Internals & Memory Management
✓ FreeAdvanced· 9 min read

Memory Leaks in Java

Java can't leak memory the way C can — but it leaks it a different way: objects that are technically reachable, and therefore un-collectable, long after they should have been forgotten.

Published September 23, 2026


Memory Leaks in Java

"Java has a garbage collector, so it can't leak memory" is a common misconception. The GC only reclaims objects that are unreachable. A Java memory leak is always the same underlying shape: something keeps an object reachable — usually accidentally — long after the application has logically finished with it.

Static collections that grow unbounded

class SessionCache {
    private static final Map<String, Session> cache = new HashMap<>(); // static — a permanent GC root
    static void put(String id, Session session) { cache.put(id, session); }
    // no corresponding remove() ever called anywhere
}

A static field is a GC root (see Garbage Collection Fundamentals) — it's reachable for as long as the class stays loaded, which for most application classes means for the entire lifetime of the JVM. Every Session ever put into this map stays reachable forever, even after the actual user session has ended, because nothing ever calls a corresponding remove(). This is leak source #1 specifically because it's so easy to write innocently — a cache that grows without any eviction policy at all.

Listener/callback leaks

Covered concretely in Observer Pattern's own memory-leak section: an observer that subscribes to a long-lived subject but is never unsubscribed stays reachable through the subject's observer list for as long as the subject lives, even if nothing else in the application still needs that observer.

ThreadLocal not cleared in pooled threads

class RequestContext {
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
    static void set(User user) { currentUser.set(user); }
    static User get() { return currentUser.get(); }
    // missing: static void clear() { currentUser.remove(); }
}

In a thread-pool-based server (Tomcat, most Spring Boot deployments), threads are reused across many requests — a pooled thread that served request A and never had its ThreadLocal cleared still carries request A's data into request B, which served on the same reused thread later. Beyond the leak, this is also a correctness bug: request B could see request A's user context. The fix is always the same: clear every ThreadLocal in a finally block (or a filter's cleanup phase) at the end of each request, not just set it at the start.

Classloader leaks

Covered in JVM Memory Areas' classloader-delegation section: when a web application is redeployed without its old classloader being fully released — because a thread, a static field in a shared framework, or a registered JDBC driver still references something loaded by the old classloader — that entire old classloader, and every class and object it ever loaded, leaks. This is a specifically severe case because it's not one leaked object, it's an entire generation of loaded classes.

Inner class implicit outer-class reference

class Activity {
    private byte[] largeData = new byte[100_000_000]; // 100MB

    class InnerHandler { // non-static inner class
        void onEvent() { /* uses Activity's state */ }
    }

    InnerHandler getHandler() { return new InnerHandler(); }
}

A non-static inner class holds an implicit reference to its enclosing instance (Activity.this), generated automatically by the compiler — even if InnerHandler never actually touches largeData or any other field of Activity. If an InnerHandler instance outlives its intent (e.g. registered as a long-lived callback somewhere), it keeps the entire enclosing Activity — including its 100MB field — reachable, purely because of the implicit reference, not because the inner class logically needs it. The fix, when the inner class doesn't need outer-instance access: make it a static nested class instead, which has no implicit outer reference at all.

String.intern() pool growth

String s = someUntrustedInput.intern(); // adds to the JVM's string pool if not already present

Before Java 7, the interned string pool lived in PermGen with a small, hard-to-tune size limit — unbounded interning of many distinct strings (e.g. interning arbitrary user input) could exhaust PermGen. Since Java 7 moved the string pool to the regular heap, this specific failure mode is far less common (the pool now grows against the much larger heap, managed by the regular GC), but unbounded interning of high-cardinality strings is still wasteful — it's just no longer the dedicated PermGen-exhaustion crisis it used to be.

Follow-up questions this topic invites — and their answers

Q: How would you find the actual leaking reference in production using a heap dump? A: Generate a heap dump (jmap -dump, jcmd GC.heap_dump, or automatically via -XX:+HeapDumpOnOutOfMemoryError — full tooling covered in GC Tuning & Diagnostics), then load it into a tool like Eclipse MAT and use its "leak suspects" report, which finds unusually large object clusters and traces their dominator path back to a GC root — that root is almost always where the actual bug lives (a static field, an un-cleared ThreadLocal, an un-removed listener).

Q: Is every long-lived cache a memory leak? A: No — a cache with an explicit eviction policy (size-bounded LRU, as in TreeMap & LinkedHashMap, or time-based expiry) is a deliberate, bounded memory tradeoff, not a leak. The distinguishing question is whether growth is bounded by design or unbounded by omission — the static SessionCache example above is a leak specifically because nothing bounds it.

Q: Why doesn't making InnerHandler static automatically break functionality that used to rely on the outer reference? A: If InnerHandler genuinely needed access to Activity's state, making it static removes that implicit access — you'd need to pass an explicit reference (or just the specific data needed) into the static nested class's constructor instead. The fix only applies cleanly when the inner class doesn't actually need outer-instance access in the first place, which is worth verifying before reflexively making every inner class static.

Q: Could a WeakReference-based cache (from JVM Memory Areas) have prevented the SessionCache leak? A: Partially — using a WeakHashMap keyed appropriately would let entries get collected once nothing else references the key, but it doesn't replace a real eviction policy (session timeout, LRU) for a cache whose keys (session IDs, likely simple strings) might otherwise stay reachable through unrelated paths — weak references solve 'nothing else needs this,' not 'this has been unused too long,' which are different conditions.

Previous

Modern Garbage Collectors

Next

GC Tuning & Diagnostics

AI Tutor

Lesson: Memory Leaks in Java

Quick actions

AI responses can be inaccurate. Verify critical information.