What is a memory leak in Java?
ThreadLocal is safe if you guaranteedly call .remove() in finally and don't pass the object reference outside the thread.
Junior Level
Memory Leak — when objects that are no longer needed remain in memory and are not removed by GC.
In C/C++ a leak is when memory is freed but a pointer remains (dangling pointer). In Java — the opposite: an object could be deleted, but a reference keeps it alive.
Simple analogy: You threw away the trash but forgot to close the lid. Trash keeps piling up even though you “threw it away.”
In Java, a leak is not “losing” memory, but **retaining it:**
- Object is no longer needed → but a reference to it remains
- GC cannot delete → there’s a reference
- Memory gradually runs out →
OutOfMemoryError
Example:
// ❌ Leak: objects added, never removed
static List<String> history = new ArrayList<>();
public void add(String data) {
history.add(data); // Grows infinitely!
// Old data is no longer needed, but stays in memory
}
Symptoms:
- May manifest as slowdown (due to frequent GC), but often no symptoms until sudden OOM.
- GC runs constantly
- Eventually:
OutOfMemoryError
When ThreadLocal is safe
ThreadLocal is safe if you guaranteedly call .remove() in finally
and don’t pass the object reference outside the thread.
Middle Level
Leak Mechanism
Leak in Java = unintended reference retention
Object alive as long as there's a path from GC Roots:
GC Root → Static Field → Map → Your Object
If you forgot to remove from Map → object alive forever!
Types of Leaks
1. On-Heap Leaks (in Heap):
// Static collections
static Map<String, Object> cache = new HashMap<>();
cache.put(key, value); // Never removed
// ThreadLocal
ThreadLocal<User> user = new ThreadLocal<>();
user.set(currentUser);
// Forgot user.remove() → object alive as long as thread lives
// Event Listeners
button.addActionListener(listener);
// Forgot removeListener → listener alive forever
2. Metaspace Leaks:
// ClassLoader leak
// On redeploy, old ClassLoader is not unloaded
→ All classes remain in Metaspace
→ OutOfMemoryError: Metaspace
3. Off-Heap Leaks (Native Memory):
// DirectByteBuffer — memory outside Heap
ByteBuffer buf = ByteBuffer.allocateDirect(100 * 1024 * 1024);
// 100 MB outside -Xmx!
// If not cleaned → OOM Killer kills the process
Diagnostics
Symptoms:
- Sawtooth Pattern — memory graph grows after each collection
- GC Thrashing — GC runs constantly, CPU 100%
- OutOfMemoryError — final result
Tools:
# Capture memory dump on OOM
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/dump.hprof
# Analyze via Eclipse MAT
→ Dominator Tree → Path to GC Roots
→ Find who's holding objects
Common sources
// 1. Static collections
static List<Data> allData = new ArrayList<>();
// 2. ThreadLocal without cleanup
ThreadLocal<Connection> conn = new ThreadLocal<>();
// In thread pool, threads are reused → data accumulates
// 3. Inner classes (implicit reference to outer class)
class Outer {
class Inner { } // Holds reference to Outer.this
}
// 4. Unclosed resources
InputStream is = new FileInputStream("file.txt");
// Forgot is.close() → buffers in memory
Senior Level
Anatomy of a Leak: Shallow vs Retained Heap
Shallow Heap = size of the object itself
Object Header (16) + fields + padding
Retained Heap = everything that would be deleted with the object
Object + all objects reachable ONLY through it
Example:
Map node: 64 bytes (Shallow)
But holds: 500 MB of data (Retained!)
→ In MAT, look at Retained Heap, not Shallow!
ClassLoader Leak Deep Dive
ClassLoader → loaded 1000 classes
Each class → InstanceKlass in Metaspace
Each class → static fields → objects in Heap
If ClassLoader is not GC'd:
→ 1000 classes remain in Metaspace
→ All static fields remain in Heap
→ All objects they reference → also
Causes:
- ThreadLocal holds object from ClassLoader
- Static field in system class references application
- JDBC driver registered in DriverManager
- Loggers hold references to application classes
Native Memory Tracking (NMT)
# Enable NMT
-XX:NativeMemoryTracking=detail
# Analyze
jcmd <pid> VM.native_memory summary
# Output:
Native Memory Tracking:
Java Heap: 2 GB
Class: 50 MB
Thread: 100 MB
Code: 80 MB
GC: 200 MB
Compiler: 30 MB
Internal: 50 MB
Symbol: 20 MB
Native Memory Tracking: 10 MB
Arena Chunk: 5 MB
Unknown: 15 MB ← Leak here?
Distributed Systems and Leaks
In a cluster, a leak can be masked:
Node 1: memory grows → load balancer switches to Node 2
Node 2: memory grows → switches to Node 3
→ Avalanche collapse of entire cluster!
Solution:
- Liveness/Readiness probes
- Fail-fast on OOME
- Memory monitoring on each node
Production Experience
Real scenario #1: ThreadLocal in Tomcat
- Web app: ThreadLocal for UserContext
- Forgot
remove()infinally - Tomcat thread pool: threads are reused
- Result: User A’s UserContext available to User B
-
- Memory Leak: Context accumulated 3 months → Metaspace OOM
These are TWO problems: (1) memory leak — Context accumulates and is never removed, (2) security breach — one user’s data accessible to another via uncleaned ThreadLocal.
Real scenario #2: DirectByteBuffer
- Netty server:
allocateDirect()for each request - GC doesn’t run (Heap almost empty)
- Native Memory: 8 GB → OOM Killer killed process
- Solution:
-XX:MaxDirectMemorySize=2g+ monitoring
Best Practices
- Always close resources (try-with-resources)
- ThreadLocal.remove() in
finally - Inner classes → static if outer not needed
- Bounded caches (Caffeine, Guava)
- HeapDumpOnOutOfMemoryError — mandatory in production
- NMT for Native Memory monitoring
- Delta Analysis — compare dumps before/after load
- Fail-fast — better to die than work with corrupted state
Senior Summary
- Leak in Java = unintended reference retention
- Shallow vs Retained Heap — look at Retained!
- ClassLoader Leaks — the most treacherous (Metaspace)
- Native Memory — not visible in Heap Dump (need NMT)
- ThreadLocal — suspect #1
- Delta Analysis — compare two dumps
- Distributed — leak can kill entire cluster
- Fail-fast > zombie state
Interview Cheat Sheet
Must know:
- Leak in Java — not losing memory, but unintended reference retention (GC cannot delete because there’s a path from GC Root)
- Shallow Heap = object’s own size; Retained Heap = everything that would be deleted with the object — in MAT look at Retained!
- Top-3 leak sources: static collections, ThreadLocal without
.remove(), unclosed resources - 3 types of leaks: On-Heap (in Heap), Metaspace (ClassLoader leaks), Off-Heap (DirectByteBuffer)
HeapDumpOnOutOfMemoryError— mandatory in production- Sawtooth Pattern: the “floor” of memory graph grows after each GC — sign of a leak
- In distributed systems, a leak on one node can cause avalanche cluster collapse
Common follow-up questions:
- How does Java leak differ from C leak? — In C, memory freed but pointer remains (dangling pointer); in Java — the opposite: object could be deleted, but reference retains it
- Why is ThreadLocal dangerous in thread pool? — Threads are reused; previous request’s data remains and is available to the next request
- What is Delta Analysis? — Comparing two heap dumps (before/after load) to identify growing classes
- Why can Inner Class cause a leak? — Non-static inner class implicitly holds a reference to
Outer.this
Red flags (DO NOT say):
- “Java can’t have memory leaks, there’s GC” — leaks happen through reference retention
- “ThreadLocal is safe because the thread will die” — In thread pool, threads do NOT die
- “Just increase -Xmx and the leak disappears” — this masks the problem, OOM will still happen
Related topics:
- [[5. When does an object become eligible for GC]]
- [[7. How can a memory leak occur in Java]]
- [[21. What is a memory leak and how to detect it]]
- [[23. What is a heap dump]]
- [[11. What is Metaspace (or PermGen)]]