Investigating an OutOfMemoryError in Production — End-to-End Playbook
Fast recall
Quick revision
Core answer
Whether you have actually run an OOM investigation under production pressure — the structured playbook reveals operational maturity
Restarting first and analyzing later (you may lose the heap dump if it wasn't on a persistent volume); or jumping to fixes before identifying which OOM subtype occurred
Distinguish all five OOM subtypes by message, mention container-specific gotchas (ephemeral filesystems, OOMKilled vs Java OOM, cgroup memory pressure), and emphasize post-fix verification with a second heap dump
Interview-ready guide
Interview answer
Investigating an OOM in production is a structured playbook with three phases: triage, analyze, fix-and-verify. The key skill is securing diagnostic evidence before restarting — beginners often restart first and lose the heap dump, especially in containers with ephemeral filesystems.
Phase 1 — Triage at the alert:
- Read the actual OOM message — there are five subtypes, each with a different root cause:
Java heap space— most common; heap is fullGC overhead limit exceeded— heap thrashing; usually a leakMetaspace— classloader leakDirect buffer memory— off-heap NIO buffer exhaustionunable to create native thread— OS thread limit
- OOMKilled in Kubernetes is not a Java OOM — kernel-level cgroup enforcement, total RSS exceeded limit
- Copy the heap dump and GC logs out of the pod before it restarts
- Capture thread dump and classloader stats if the JVM is still alive
- Restart to restore traffic
Phase 2 — Analyze offline:
- Open heap dump in Eclipse MAT, run Leak Suspects — typically identifies the culprit in seconds
- Use Dominator Tree sorted by retained heap; Path to GC Roots excluding weak/soft references
- Cross-reference with GC logs in GCEasy — confirm steady upward trend in post-full-GC baseline
- For Metaspace OOM: classloader stats; for direct buffer OOM: Native Memory Tracking
Phase 3 — Fix and verify:
- Common fixes: bound caches with Caffeine, add
ThreadLocal.remove()in finally, replacenewCachedThreadPool()with bounded pools, fix classloader retention - Deploy fix, run under load for hours, capture another heap dump
- Confirm retained heap of the suspect class is now bounded and stable
- Post-mortem: update runbooks, add observability alerts, verify diagnostic flags fleet-wide
The discipline that separates senior engineers:
- Securing evidence before restart
- Reading the OOM subtype correctly to choose the right diagnostic tool
- Verifying the fix with a second heap dump rather than trusting that 'no OOM' means it's fixed
In an interview, the differentiator is mentioning OOMKilled vs Java OOM — most candidates conflate them, missing the fact that they require different fixes.
Investigating an OutOfMemoryError in Production — End-to-End Playbook
Direct Answer
On OutOfMemoryError: enable -XX:+HeapDumpOnOutOfMemoryError and GC logging before restart. Read the OOM message (heap, metaspace, direct buffer, unable to create native thread). Correlate dump dominators with recent deploys and traffic. For metaspace, check dynamic class generation; for heap, MAT leak suspects and allocation profiling.
Common Mistakes & Pitfalls
Restarting first and analyzing later (you may lose the heap dump if it wasn't on a persistent volume); or jumping to fixes before identifying which OOM subtype occurred
Check your understanding
Answer these without looking back at the article.
- How do you distinguish OOMKilled from a Java OutOfMemoryError in Kubernetes?
- What is Native Memory Tracking and when do you enable it?
- Why is `Executors.newCachedThreadPool()` an anti-pattern in production?
- How do you ensure a heap dump survives a Kubernetes pod restart?
Related interview prep categories
2Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.