Every OutOfMemoryError and What It Means
Each OutOfMemoryError message, what it actually indicates, the most likely cause, and the first three things to check — plus why OOMKilled by the kernel is a different failure entirely.
On this page
OutOfMemoryError is not one error. The message after the colon tells you which region ran out, and
each region has a different set of causes and a different first thing to check.
Key Takeaways
- The message names the region.
Java heap space,Metaspace,Direct buffer memoryandunable to create native threadare four unrelated problems. GC overhead limit exceededis heap exhaustion with an earlier warning.unable to create native threadis usually an OS or thread-count limit, not a heap problem — and lowering-Xmxcan fix it.- OOMKilled (exit 137) is the kernel, not the JVM. No stack trace, no heap dump.
- Always run production with
-XX:+HeapDumpOnOutOfMemoryError.
The field guide
| Message | Region | Most likely cause |
|---|---|---|
Java heap space | Heap | Leak, undersized heap, or one huge allocation |
GC overhead limit exceeded | Heap | Same, caught slightly earlier |
Requested array size exceeds VM limit | Heap | An array over ~2 billion elements |
Metaspace | Native | Classloader leak, or heavy dynamic class generation |
Compressed class space | Native | Over 1GB of class metadata with compressed oops |
unable to create native thread | Native | OS thread limit, or no address space left for stacks |
Direct buffer memory | Native | Unpooled NIO buffers, or MaxDirectMemorySize too low |
reason stack_trace_with_native_method | Native | A JNI or native library allocation failed |
Java heap space
java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3745)
at java.base/java.lang.StringBuilder.append(StringBuilder.java:172)
at com.acme.ReportBuilder.render(ReportBuilder.java:88)The stack trace shows where the allocation failed, which is rarely where the memory went. A
StringBuilder.append at the top usually means the heap was already nearly full and this happened to
be the next request.
Three causes, distinguished by the GC log:
A leak — occupancy after each Full GC climbs. Go to The seven classic memory leaks.
An undersized heap — occupancy after Full GC is stable but high. The working set genuinely does
not fit. Raise -Xmx, or reduce what is held live.
One large allocation — the heap was fine and a single request asked for too much. Loading a
200MB CSV with Files.readAllBytes, a query with no LIMIT, an unbounded IN clause. The fix is
streaming and pagination, not a bigger heap.
// Materialises every row. One request can exhaust the heap.
List<Order> all = repository.findAll();
// Streams, with a fetch size, releasing each row after processing.
try (Stream<Order> stream = repository.streamAll()) {
stream.forEach(this::process);
}GC overhead limit exceeded
Thrown when the JVM has spent over 98% of recent wall-clock time collecting and recovered under 2% of the heap. It is a mercy: without it, the application would simply stop responding while technically still running, which is far harder to diagnose than a crash.
Treat it identically to Java heap space. Do not disable it with
-XX:-UseGCOverheadLimit — that converts an immediate, diagnosable failure into a process that
consumes a whole CPU core and serves nothing.
Metaspace
Class metadata exhausted. Two realistic causes:
A classloader leak. Common in application servers that redeploy, and in anything that creates classloaders at runtime. Each redeploy adds a full copy of the application's classes, retained by one stray reference.
Dynamic class generation. Heavy proxying (CGLIB, ByteBuddy), scripting engines, and some serialisation and mocking libraries generate classes at runtime. A cache-per-instance rather than cache-per-class bug produces an unbounded stream of generated classes.
jcmd <pid> VM.metaspace summary
jcmd <pid> GC.class_stats # needs -XX:+UnlockDiagnosticVMOptions
-Xlog:class+load=info # what is being loaded, and by which loader
-XX:MaxMetaspaceSize=512m # bound it so it fails fast and diagnosablyIf the loaded-class count grows without bound over hours, it is one of the two causes above.
unable to create native thread
This is the counterintuitive one: it is usually not a heap problem, and raising -Xmx can make it
worse, because a larger heap leaves less address space for thread stacks.
# 1. How many threads does the process have?
ls /proc/<pid>/task | wc -l
# 2. What is the OS limit?
ulimit -u # max user processes
cat /proc/sys/kernel/threads-max
cat /sys/fs/cgroup/pids.max # containers have their own PID limit
# 3. Where are they coming from?
jcmd <pid> Thread.print | grep '^"' | sed 's/".*//' | sort | uniq -c | sort -rnThat last command groups thread names, and the answer is almost always visible immediately: a pool
created per request, an HTTP client instantiated in a loop, a newCachedThreadPool under a traffic
spike, or a scheduler that is never shut down.
Fixes: bound the thread creation (the real fix); reduce -Xss from 1MB to 512k or 256k to fit more
threads; raise the OS or cgroup limit; or move to virtual threads.
Direct buffer memory
Off-heap NIO buffers exhausted. -XX:MaxDirectMemorySize defaults to the value of -Xmx, so a 4GB
heap silently permits another 4GB of direct memory.
Direct buffers are freed only when their Java wrapper is garbage collected, so if the heap is not under pressure the collector has no reason to run and native memory keeps growing. Netty's pooled allocator exists precisely for this; the usual application-level bug is allocating a direct buffer per request instead of pooling.
jcmd <pid> VM.native_memory summary # requires -XX:NativeMemoryTracking=summary
# Also exposed as a JMX bean: java.nio:type=BufferPool,name=directOOMKilled — a different failure
$ kubectl describe pod api-7f8d
Last State: Terminated
Reason: OOMKilled
Exit Code: 137No Java stack trace, no heap dump, no application log line — the kernel sent SIGKILL. The JVM never
knew it was in trouble, which means its own limits were never reached: the process exceeded the
cgroup limit while the heap was fine.
The cause is almost always non-heap memory being unaccounted for. Container limit 2GB and -Xmx2g
leaves nothing for Metaspace, thread stacks, the code cache, direct buffers and GC structures — all of
which are covered in JVM memory areas.
# Let the JVM read the container limit and take a percentage of it
-XX:MaxRAMPercentage=70.0
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=256m
-XX:NativeMemoryTracking=summaryDetails in JVM flags and container limits.
Always be ready
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump.hprof
-XX:+ExitOnOutOfMemoryError # or CrashOnOutOfMemoryError
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=20MThe heap dump costs nothing until it fires, and without it you are guessing. Note the path must be on a volume that survives the container, and the disk must have room for a file the size of the heap.
ExitOnOutOfMemoryError is worth arguing for: after an OutOfMemoryError, a JVM may keep running
with some threads dead and some objects half-initialised, serving errors while passing health checks.
Dying immediately and letting the orchestrator restart the pod is usually the better outcome. In
Kubernetes, pair it with a heap dump written to a persistent volume so the evidence outlives the pod.
What gets asked
"What types of OutOfMemoryError are there?" is common, and naming four or five with distinct causes
is a good answer. The stronger question is scenario-based: "your pod restarts with exit code 137 but
there is nothing in the logs" — the answer is OOMKilled, non-heap memory unaccounted for, and the
fix is MaxRAMPercentage plus bounding Metaspace and direct memory.
Frequently Asked Questions
Should you ever catch OutOfMemoryError?
What is the difference between OutOfMemoryError and OOMKilled?
What causes GC overhead limit exceeded?
Related tutorials
- The Seven Classic Java Memory LeaksThe seven leak patterns that recur in every codebase, why a garbage-collected language leaks at all, how each one is diagnosed from a heap dump, and the code change that fixes each.
- Heap Dump Analysis with MATCapturing a heap dump safely in production, the difference between shallow and retained size, reading the dominator tree, using path to GC roots, and OQL queries that answer real questions.
- Reading GC Logs and Tuning Without GuessingEnabling unified GC logging, reading a G1 log line by line, calculating allocation and promotion rates, identifying every Full GC cause, and the tuning changes that are usually wrong.
- JVM Flags and Container Awareness in KubernetesHow the JVM reads cgroup limits, why MaxRAMPercentage beats Xmx in a container, how CPU quota affects GC and pool sizing, and why CPU limits cause latency spikes through throttling.