What is a memory leak and how to detect it?
4. Understand why they're not being removed
Junior Level
Memory Leak — objects that are no longer needed but remain in memory.
Simple analogy: You threw away the trash but forgot to close the door. Trash keeps piling up.
Symptoms:
- Application gets slower and slower
- GC runs constantly
OutOfMemoryErroreventually
How to detect:
- Enable dumps:
-XX:+HeapDumpOnOutOfMemoryError - Open dump in Eclipse MAT
- Find the largest objects
- Understand why they’re not being removed
Middle Level
Delta Analysis Methodology
1. Baseline: dump after startup and warmup
2. Load: stress test
3. Cooldown: System.gc() for forced cleanup (ONLY for diagnostics, NOT in production!)
4. Snapshot: second dump
5. Compare: which classes grew?
Shallow vs Retained Heap
Shallow Heap = object's own size
Retained Heap = everything that would be deleted with it
Example:
Map node: 64 bytes (Shallow)
Holds: 500 MB of data (Retained!)
→ In MAT, look at Retained Heap!
Tools
| Tool | When | Overhead |
|---|---|---|
| Eclipse MAT | Dump analysis | Offline (0%) |
| JFR/JMC | Production monitoring | < 1% |
| jcmd | Quick check | Low |
| JProfiler | Development | High |
Common Leak Sources
// 1. Static collections
static Map<String, Object> cache = new HashMap<>();
// 2. ThreadLocal
ThreadLocal<User> user = new ThreadLocal<>();
// Forgot remove()
// 3. Listeners
eventBus.subscribe(listener);
// Forgot unsubscribe
// 4. Unclosed resources
InputStream is = new FileInputStream("file");
// Forgot close()
Senior Level
Dominator Tree
MAT: Dominator Tree
→ Shows objects retaining the most memory
→ Not necessarily the largest themselves
→ Small object can hold gigabytes!
Path to GC Roots
MAT: Path to GC Roots (exclude weak/soft)
→ Shows reference chain to root
→ Immediately see who's holding the object
Typical leak roots:
- Static field → Map → your object
- ThreadLocal → your object
- Thread → your object
OQL (Object Query Language)
-- Find all strings > 1000 characters
SELECT s.value.toString()
FROM java.lang.String s
WHERE s.value.length > 1000
-- Find duplicate strings
SELECT toString(s.value) as val, count(*) as cnt
FROM java.lang.String s
GROUP BY toString(s.value)
HAVING count(*) > 100
JFR OldObjectSample
Java Flight Recorder:
→ OldObjectSample event
→ Shows objects that survived many GCs
→ Visualization of path to GC Roots
→ WITHOUT taking a dump!
→ The only safe method for Heap > 100 GB
Sawtooth Pattern
Sawtooth Pattern: the "floor" (minimum usage after GC) grows from cycle to cycle.
This means: after each collection, more objects remain — a leak.
Memory graph after GC:
Normal:
|\ |\ |\
| \ | \ | \
|__\ |__\ |__\
← bottom at same level
Leak:
|\ |\ |\
| \ | \ | \
|__\ |___\ |____\
← bottom rising!
Production Experience
Real scenario: ClassLoader Leak
- Tomcat redeploy 10 times → Metaspace 256 MB → 4 GB
- Cause: ThreadLocal held object from application ClassLoader
- Solution: ThreadLocal.remove() in ServletContextListener
Best Practices
- Delta Analysis — compare two dumps
- Retained Heap > Shallow Heap
- Path to GC Roots → exclude weak/soft
- JFR OldObjectSample for large Heaps
- Sawtooth Pattern → monitoring
- HeapDumpOnOutOfMemoryError — mandatory
- Automate dump taking
Senior Summary
- Delta Analysis = dump comparison before/after
- Retained Heap = real memory impact
- Dominator Tree = finding the culprit
- Path to GC Roots = reference chain
- JFR OldObjectSample = dump alternative for large Heaps
- Sawtooth Pattern = visual leak indicator
- ClassLoader Leaks = the most treacherous
Interview Cheat Sheet
Must know:
- Memory Leak in Java — unintended reference retention; GC doesn’t remove because there’s a path from GC Root
- Delta Analysis: Baseline (dump after warmup) → Load → Cooldown → Snapshot → Compare growing classes
- Shallow Heap = object size; Retained Heap = everything deleted with it → in MAT look at Retained!
- Sawtooth Pattern: the “floor” of memory graph grows after each GC — visual leak indicator
- Dominator Tree (MAT): shows objects retaining most memory; small object can hold gigabytes!
- Path to GC Roots (exclude weak/soft): shows reference chain to root → 99% of leaks = Static or Thread
- JFR OldObjectSample: shows objects that survived many GCs; WITHOUT taking dump; for Heap > 100 GB
Common follow-up questions:
- Why is Retained Heap more important than Shallow Heap? — Map node: 64 bytes Shallow, but holds 500 MB Retained; removing node frees 500 MB
- Why Delta Analysis instead of single dump? — Single dump shows “what exists”; two dumps show “what grows” — the leak
- Why is JFR better than dump for large Heaps? — 128 GB dump = STW 30-60 seconds; in Kubernetes Liveness Probe timeout → pod killed; JFR < 1% overhead
- What is OQL? — Object Query Language: SQL-like queries to dump (find large strings, duplicates, ThreadLocal leaks)
Red flags (DO NOT say):
- “I take dump in production on 128 GB without warning” — STW 30-60 seconds, Liveness Probe fail → pod killed
- “System.gc() before Delta Analysis — normal practice” — NOT in production! Only in diagnostics, and carefully
- “Shallow Heap shows real object impact” — Retained Heap shows how much would be freed on removal
Related topics:
- [[6. What is a memory leak in Java]]
- [[7. How can a memory leak occur in Java]]
- [[22. What tools help analyze memory]]
- [[23. What is a heap dump]]
- [[25. What are GC roots]]