Question 21 · Section 3

What is a memory leak and how to detect it?

4. Understand why they're not being removed

Language versions: English Russian Ukrainian

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
  • OutOfMemoryError eventually

How to detect:

  1. Enable dumps: -XX:+HeapDumpOnOutOfMemoryError
  2. Open dump in Eclipse MAT
  3. Find the largest objects
  4. 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

  1. Delta Analysis — compare two dumps
  2. Retained Heap > Shallow Heap
  3. Path to GC Roots → exclude weak/soft
  4. JFR OldObjectSample for large Heaps
  5. Sawtooth Pattern → monitoring
  6. HeapDumpOnOutOfMemoryError — mandatory
  7. 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]]