Quick fix
# 1. Enable heap dump first (lands automatically on next OOM), faster than guessing
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/app.hprof -jar app.jar
# 2. GC log shows which area is exhausted
java -Xlog:gc -jar app.jar # or -verbose:gc
# 3. Common quick wins by signature
# heap space + big file download -> stream or external link, never whole byte[]
# heap space + big query -> paginate + cache
# thread exhaustion -> bounded pool; logback discardingThreshold back to default (not 0)
# random OOM -> check -Xmn, never equal -Xmx
What you will run into
A Java service suddenly dies / returns 502 / the API answers 500, and the logs show various OOM errors. Different signatures point to completely different root causes - match the signature first:
| Error signature | Typical root cause | Where to look |
|---|---|---|
OutOfMemoryError: Java heap space | Heap filled by big objects/collections: one-shot read of a 200MB file into byte[], an unpaginated 100k-row query, in-memory ZIP-bomb unzip, or -Xmn equal to -Xmx | heap dump + MAT, find who owns the memory; check big files / big queries / unzip |
OutOfMemoryError: Metaspace | All XSDs/classes loaded once at startup (JAXB newSchema full init) fills Metaspace with class metadata | check startup schema/class count; resize or chunked loading |
StackOverflowError | recursion without an exit: trusting a third-party parameter (they sent -1) | parameter validation + recursion to loop |
OutOfMemoryError: unable to create new native thread | thread count over the OS limit: unbounded pool + blocked logging (disk full + logback AsyncAppender queue full) | jstack thread count/stacks; disk, logging config, pool caps |
OutOfMemoryError: GC Overhead limit exceeded | GC churns with zero reclaim: memory leak (e.g. ThreadLocal not cleared), Full GC can't free anything | MAT Incoming references (e.g. ThreadLocalMap) |
| Random OOM at different places | JVM flag imbalance: -Xmn (young gen) equals -Xmx | check startup script -Xmn/-Xmx ratio |
Why this happens
JVM memory has four independent areas - heap / metaspace / thread stacks / native threads - each can be exhausted on its own:
| Area | Exhaustion mechanism |
|---|---|
| Heap | objects keep accumulating: whole-file reads, unpaginated big queries, ZIP bombs (tiny archive inflates to 1000x+) |
| Metaspace | class metadata: one-shot newSchema of all XSDs loads many classes |
| Thread stacks | deep recursion: StackOverflowError |
| Native threads | OS thread limit (ulimit -u / /proc/sys/kernel/threads-max): unbounded pool + business threads blocked on logging (disk full + AsyncAppender queue full with discardingThreshold=0) |
| GC churn | leaked objects cannot be freed: Full GC reclaims <2% while taking >98% of the time -> GC Overhead limit |
JVM "memory" is not one thing — it is four independent exhaustion channels, each with different OOM text and different fixes: the Java heap is GC-managed and only exhausts when objects accumulate;
Metaspace stores class metadata and exhausts when classes are generated dynamically (e.g.
full XSD→classes); thread stacks are fixed per thread (~1MB class) and exhaust via deep recursion (StackOverflowError);
native threads are bounded by the OS thread limit (ulimit -u / threads-max) and exhaust when an unbounded pool only grows (threads blocked on log writes).
GC thrashing (reclaiming <2% while using >98% of time) is the fingerprint of "many unreachable-but-uncollected objects".
Deterministic takeaway: the region name in the OOM text (Java heap space / Metaspace / unable to create native thread / GC overhead) decides the direction — heap OOM: object accumulation and leaks;
Metaspace OOM: class-loading volume; native-thread OOM: thread count and blocking;
do not blanket-apply "-Xmx"; fixed diagnostic: read the exception class name before acting.
Step by step
1. Record the signature (decide the class first) — Required
Java heap space / Metaspace / StackOverflowError / unable to create new native thread / GC Overhead limit exceeded / Direct buffer memory - different signature, different weapon.
2. Reproduce and collect evidence — Required
- Enable
-XX:+HeapDumpOnOutOfMemoryErrorto get a .hprof on OOM; jmap -dump:format=b,file=app.hprof <pid>while the service is still alive;- Open the dump in MAT: Dominator Tree big objects + Incoming references (case 8: ThreadLocal leak = Incoming references point to ThreadLocalMap);
jstack <pid>for thread stacks (check total threads when native threads are exhausted).
3. Fix by root cause — Required
| Root cause | Fix |
|---|---|
whole-file read into memory (readFileContent full byte[]) | stream it (buffered FileInputStream->OutputStream) or move the file to FTP/object storage as an external link |
| unpaginated big query (100k+ rows repeatedly) | row limit + query cache + longer polling interval |
| ZIP bomb (tiny archive, huge inflation) | forbid nested archives, validate file size and paths, extract to disk with a total cap |
| Metaspace (all XSDs loaded once) | temporary -XX:MaxMetaspaceSize bump; real fix: service split / load schemas on demand |
| recursion without exit (third party sent -1) | strict parameter validation + recursion to loop |
| unbounded pool + blocked logging | bounded pool; AsyncAppender discardingThreshold back to a sane value (0 = never drop, but also no backpressure protection; disk full stalls threads) |
-Xmn = -Xmx | restore a sane ratio (young gen about 1/3~1/2 of the heap, tune by workload) |
| ThreadLocal leak | remove in finally; pooled threads otherwise pin references forever |
4. Verify — Required
Re-run the original trigger path under load: no OOM, stable GC log, thread count back to normal.
How to confirm it works
The same trigger path no longer OOMs, the GC log shows no consecutive zero-reclaim Full GCs, the thread count is stable, and the old big objects / leak references are gone from a fresh dump.
Other environments
| Environment | Notes |
|---|---|
| modern JVM (G1/ParallelGC) | with a tiny heap and fast allocation the GC Overhead signature may render as plain Java heap space (verified locally); do not only grep for the literal signature |
| Not verified | Metaspace / native thread / unbounded pool + logback blocking scenarios (no matching production environment); mechanisms and fixes come from source cases 2/4 |
Symptom
Java service OOMs: Java heap space (big file / big query / ZIP bomb / -Xmn imbalance), Metaspace (full schema load), StackOverflowError (recursion without exit), unable to create new native thread (thread exhaustion), GC Overhead limit exceeded (GC churn).
Cause
Each JVM memory area exhausts independently: heap filled by big objects/collections, Metaspace by class metadata, thread stacks by deep recursion, native threads over the OS limit, and leaked objects make GC churn with zero reclaim.
Solution
- Enable
-XX:+HeapDumpOnOutOfMemoryError+ GC log first; - Match the error signature to its class (see the cheat-sheet);
- Stream big files / external links, paginate + cache big queries, block ZIP bombs (size/nesting/path checks), chunked schema loading, parameter validation + recursion to loop, bounded thread pool + non-blocking logging, sane -Xmn ratio, remove ThreadLocal in finally;
- Prove with MAT/jstack, then verify under load.
Verification Evidence
- Mechanism repro (2026-08-30, WSL Ubuntu, OpenJDK 17.0.18): 1)
-Xmx64munbounded allocation ->OutOfMemoryError: Java heap space; 2) infinite recursion ->StackOverflowError(depth 22691); 3) ZIP bomb (200MB of repeated chars compressed to 204KB, ratio >1000x) inflated in memory ->Java heap space(case-1 mechanism); 4)-Xmx16mslow leak -> 31 Full GCs in 16s with 14M->14M zero reclaim (GC-churn mechanism verified; literal GC Overhead signature not stably reproduced). local repro log 2026-08-30. - Not verified: Metaspace / native thread / unbounded pool + logback blocking (no matching environment); mechanisms and fixes from source cases 2/4.
Applicable tools (1)
This problem is linked from the tool pages below — reachable from either side.
jdkSource & evidence
Source: https://www.cnblogs.com/mylibs/p/19321491/production-accident-vl01
← Coding Agents & Terminal domain | Problem Catalog Experience | Facing another issue? Go to the Helpdesk