What happens on OutOfMemoryError
An OutOfMemoryError (OOME) is a fatal JVM error (extending java.lang.VirtualMachineError) thrown when the Java Virtual Machine cannot allocate sufficient physical or virtual mem...
🟢 Junior Level
An OutOfMemoryError (OOME) is a fatal JVM error (extending java.lang.VirtualMachineError) thrown when the Java Virtual Machine cannot allocate sufficient physical or virtual memory for an object, and no additional memory can be made available by the Garbage Collector.
Real-World Analogy
Think of an overcrowded passenger ferry:
- As passengers board (object allocations), the ferry approaches its maximum legal displacement limit.
- The crew throws all non-essential cargo, deck chairs, and luggage overboard (Full GC runs; all
SoftReferenceinstances purged). - If passengers continue pouring onto the dock and the boat has zero remaining capacity, the captain issues an emergency alarm (
OutOfMemoryError).
The Internal JVM OOM Escalation Sequence
Before throwing an OutOfMemoryError, the JVM executes a strict escalation ladder to exhaust all possible recovery mechanisms:
Mutator thread requests allocation (e.g., `new byte[100MB]`)
│
▼ (Allocation fails in TLAB / Eden)
Collector triggers Minor / Mixed GC
│
▼ (Still insufficient contiguous memory)
JVM triggers emergency Stop-The-World Full GC
│
▼
All `SoftReference` instances across the entire heap are forcefully purged
│
▼
JVM evaluates the GC Overhead Limit Heuristic:
Did GC spend >98% of CPU time while reclaiming <2% of heap?
├── YES ──> Throws `OutOfMemoryError: GC overhead limit exceeded`
└── NO
│
▼
Is there still insufficient contiguous memory to satisfy the allocation?
├── YES ──> Throws `OutOfMemoryError: Java heap space`
└── NO ──> Allocation succeeds; execution resumes
🟡 Middle Level
The “Zombie State” Danger: Why the JVM Does NOT Crash by Default
A dangerous misconception among developers is that throwing an OutOfMemoryError automatically shuts down the JVM.
[!CAUTION] The Zombie Application Hazard: By default, an
OutOfMemoryErrorterminates ONLY the specific thread that requested the allocation. The rest of the JVM process and all other application threads continue running!This frequently leaves the system in a crippled, corrupted Zombie State:
- Orphaned Synchronization Monitors: If a thread dies while holding an explicit lock (or before completing a
try-finallyblock), the lock is never released, causing permanent Deadlocks for surviving worker threads.- Corrupted Invariants: Shared collections (
ConcurrentHashMap, database caches, or state machines) are left partially updated with broken data integrity.- Degraded Thread Pools: If worker threads in a
ThreadPoolExecutordie from OOMEs, the pool capacity silently shrinks until no threads remain to service incoming requests.
Production Fail-Fast Strategy
Never attempt to handle or recover from an OutOfMemoryError inside Java application code. Instead, adopt a strict Fail-Fast production architecture: let the container die cleanly and emit a diagnostic memory dump so orchestrators (Kubernetes) can replace it with a healthy replica.
# Mandatory Production Flags for Containerized JVMs:
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/dumps/oom_dump.hprof \
-XX:+ExitOnOutOfMemoryError \
-XX:OnOutOfMemoryError="sh /scripts/notify_and_kill.sh %p" \
-jar app.jar
-XX:+HeapDumpOnOutOfMemoryError: Automatically writes an.hprofmemory snapshot at the exact moment of failure.-XX:HeapDumpPath: Path to persistent storage (e.g., a Kubernetes Persistent Volume) so the dump survives container restart.-XX:+ExitOnOutOfMemoryError: Forces the JVM process to terminate immediately on the first OOME, preventing corrupted Zombie States.-XX:+CrashOnOutOfMemoryError: Alternative flag that produces a native core dump andhs_err_pid.logfile.
🔴 Senior Level
JVM OutOfMemoryError vs. Linux Kernel OOM-Killer (Exit Code 137)
In cloud-native Kubernetes environments, engineers frequently confuse a Java-level OutOfMemoryError with an operating system kernel termination:
| Dimension | JVM OutOfMemoryError | Linux Kernel OOM-Killer |
|---|---|---|
| Originating Entity | Java Virtual Machine execution engine | Linux OS Kernel (cgroup memory controller) |
| Trigger Cause | Java Heap, Metaspace, or Direct Memory exhausted | Total Process RSS exceeds container cgroup memory limit |
| Termination Signal | Standard Java exception propagation | Uncatchable OS signal SIGKILL (Signal 9, Exit Code 137) |
| Stack Trace in Logs | Yes (java.lang.OutOfMemoryError: ...) |
None (Process vanishes instantly without writing Java logs) |
| Heap Dump Created | Yes (via -XX:+HeapDumpOnOutOfMemoryError) |
No (JVM is killed before it can write an .hprof dump) |
| Root Cause Location | Java application logs, GC logs, Heap Dump | Linux system logs: dmesg -T \| grep -i oom |
# Diagnosing a Linux Kernel OOM-Killer event in container logs:
$ dmesg -T | grep -i "killed process"
# [Sat Sep 26 14:30:00 2026] Memory cgroup out of memory: Kill process 1234 (java) score 1024
# Killed process 1234 (java) total-vm:4582912kB, anon-rss:2097152kB, file-rss:0kB
Why catch (OutOfMemoryError e) is an Anti-Pattern
Attempting to catch and recover from an OOME in business code is considered an egregious anti-pattern:
// ❌ DANGEROUS ANTI-PATTERN: Attempting to catch OOME
try {
processLargeDataset();
} catch (OutOfMemoryError e) {
logger.error("Out of memory, clearing cache...", e); // 💥 Secondary OOME!
cache.clear();
}
Why Recovery Fails:
- Secondary OOME in Handlers: Logging the error (
logger.error(...)) allocates newString,char[], and log event objects. When memory is exhausted down to the last byte, attempting to allocate log objects throws a secondary uncatchable OOME, crashing the handler. - Concurrent Collateral Damage: While the failing thread threw an error, dozens of concurrent worker threads were also allocating memory. Some of those threads may have failed silently or left data structures corrupted.
- Empty Stack Traces: When heap memory is exhausted, the JVM cannot allocate the
StackTraceElement[]array needed to construct the exception’s stack trace! The JVM falls back to throwing a pre-allocated static singletonOutOfMemoryErrorwith zero stack trace information.
Behavior of finally Blocks During OOME
When an OOME is thrown in a try block:
- The JVM attempts standard stack unwinding, entering the
finallyblock. - The Catch: If the
finallyblock executes any code that allocates an object, or if calling a method insidefinallyrequires expanding the thread stack, the JVM throws anotherOutOfMemoryError, terminating thefinallyblock prematurely! - You can never rely on
finallyblocks to release resources or persist state once an OOME occurs.
4 Tricky Questions
1. Does an OutOfMemoryError automatically shut down the JVM process, and what is the “Zombie State”?
Answer: No. By default, the JVM does not terminate when an OutOfMemoryError is thrown. The error propagates up the call stack of the allocating thread only, causing that specific thread to terminate while all other threads in the JVM continue running. This often leaves the application in an unstable “Zombie State”: worker threads in thread pools silently die, locks held by the terminated thread may never be released, and shared caches remain in half-mutated, corrupted states. Production environments prevent this by passing -XX:+ExitOnOutOfMemoryError to ensure the entire JVM terminates immediately upon the first OOM occurrence.
2. How can an OutOfMemoryError be thrown with a completely empty stack trace?
Answer: Constructing a full Java stack trace requires allocating memory: the JVM must instantiate StackTraceElement objects for every active frame on the call stack and populate string representations of class and method names. When an application encounters severe heap exhaustion, there is literally zero memory available to instantiate these stack trace objects. To handle this without triggering an infinite recursion of allocation failures, the JVM falls back to throwing a pre-allocated, static singleton instance of java.lang.OutOfMemoryError that has an empty stack trace array.
3. Why does catching OutOfMemoryError in a try-catch block fail to guarantee application recovery?
Answer: Catching OutOfMemoryError fails for three fundamental reasons:
- Secondary Allocation Failures: Handling the error (e.g., formatting log messages, creating error DTOs, or closing network streams) requires allocating objects. In an exhausted heap, executing the catch block immediately triggers a secondary OOME.
- Shared State Corruption: The allocation failure may have interrupted a critical section in mid-execution, leaving internal invariants, hash tables, and database buffers in an inconsistent state.
- Unreleased Locks: If the thread was terminated while holding low-level synchronization locks before reaching a cleanup handler, other application threads waiting on those locks will hang indefinitely in deadlocks.
4. How do you distinguish between a Java-internal OutOfMemoryError and an operating system Linux OOM-Killer termination (Exit Code 137)?
Answer:
- Java-Internal OOME: The JVM remains in control. It logs a full Java error with a message (e.g.,
Java heap spaceorMetaspace), executes-XX:+HeapDumpOnOutOfMemoryErrorto write an.hprofheap dump file, and logs GC events to the GC log. - Linux OOM-Killer (Exit Code 137): The container process breaches its cgroup memory limit (RSS > memory ceiling) due to non-heap memory (thread stacks, direct buffers, Metaspace). The Linux kernel immediately issues an uncatchable
SIGKILL(Signal 9: $128 + 9 = 137$). The Java process terminates instantly from the outside: it cannot write an.hprofdump, and no Java stack trace is emitted. The event is verified by runningdmesg -T | grep -i oomon the host node.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“An
OutOfMemoryErroroccurs when the JVM cannot satisfy an object allocation even after executing an emergency Full GC and forcefully clearing allSoftReferenceinstances. By default, OOME terminates only the thread that failed the allocation, leaving the application in a corrupted Zombie State with deadlocks and damaged caches. In production, never attempt to catch OOME; adopt a Fail-Fast approach using-XX:+HeapDumpOnOutOfMemoryErrorand-XX:+ExitOnOutOfMemoryError. Furthermore, distinguish an internal Java OOME from a Linux OOM-Killer termination (Exit Code 137), which sends an instantSIGKILLwhen total process RSS breaches the container memory limit.”
Key Architectural Takeaways
- The Escalation Path: TLAB fails $\to$ Minor GC $\to$ Full GC $\to$ Clear SoftReferences $\to$ Evaluate GC overhead limit $\to$ Throw OOME.
- Zombie State Risk: OOME terminates only the allocating thread; surviving threads continue running in an unstable state.
- Empty Stack Traces: Under total memory exhaustion, the JVM cannot allocate
StackTraceElement[]and throws a pre-allocated singleton with no stack trace. - Exit Code 137 (OOM-Killer): Caused by Linux cgroup RSS limits; leaves zero Java logs and no heap dump.
- Production Standard: Always configure
-XX:+HeapDumpOnOutOfMemoryErrorand-XX:+ExitOnOutOfMemoryError. finallyVulnerability: Allocations insidefinallyblocks will fail with secondary OOMEs during unwinding.
Red Flags & Anti-Patterns
- 🚩 Catching
OutOfMemoryErrorin business logic: Leads to secondary allocation crashes, corrupted invariants, and deadlocks. - 🚩 Assuming an OOME automatically terminates the entire JVM: Fails to recognize partial thread death and Zombie States.
- 🚩 Confusing a Java Heap OOME with a Linux Container OOM-Killer (Exit Code 137): Wastes hours searching for heap leaks when the culprit is off-heap native memory.
- 🚩 Storing heap dumps in ephemeral container layers: The dump is deleted when Kubernetes restarts the pod; always mount a persistent volume.
- 🚩 Relying on
finallyblocks for transactional safety during OOM: Any allocation insidefinallyaborts execution immediately.
Related Topics
- What types of OutOfMemoryError exist — Comprehensive taxonomy of OOM variants
- What are -Xms and -Xmx parameters — Sizing heap memory boundaries
- What is a heap dump — Capturing and analyzing HPROF memory snapshots
- What is a memory leak in Java — Unintentional reference retention
- What is Metaspace (or PermGen) — Metaspace exhaustion mechanics