🔴 The Error You're Seeing

Confirm this matches your console output. If it does, you're in the right place.

ERROR LOGException in thread "main" java.lang.OutOfMemoryError: Java heap space at OomHeap.main(OomHeap.java:8) // Variant: thread creation fails once OS limits are hit (captured on JDK 25): [0.194s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 512k, guardsize: 16k, detached. [0.195s][warning][os,thread] Failed to start the native thread for java.lang.Thread "Thread-4067" threads started before failure: 4067 Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached at java.base/java.lang.Thread.start0(Native Method) at java.base/java.lang.Thread.start(Thread.java:1417) at OomNativeThread.main(OomNativeThread.java:9)

⚡ Quick Fix Works 80% of the time

Capture a heap dump BEFORE restarting, then size the heap from measured live set — never bump -Xmx blind.

# On the struggling process (pid), capture evidence first: jcmd <pid> GC.heap_dump /tmp/heap.hprof # Then restart with headroom sized ~2-3x steady-state live set: java -Xms4g -Xmx4g -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/tmp -jar app.jar

🧠 Why this Happens

Tap to expand the deep technical explanation

When the allocator cannot satisfy a request, the JVM triggers full GC cycles asking every collector to free space; if references survive (a leak) or the workload genuinely exceeds the ceiling, collection frees less than the request needs and HotSpot throws OutOfMemoryError from deep inside the allocation path — which is why frames like ArrayList.grow appear even though collections are innocent bystanders. The variants come from DIFFERENT pools: heap space from the object arena governed by -Xmx, Metaspace from class metadata storage off-heap, GC overhead limit from a policy timer counting time spent collecting versus reclaimed, and native-thread exhaustion from the operating system refusing pthread_create long before any Java memory pool fills. One error name, four unrelated resource ceilings.

The HITEC City Parking Spot Analogy:

Heap space is a warehouse rented for boxes: either you hoard boxes you will never ship again (leak — throw them out) or the warehouse is honestly too small for the business (size it up). Metaspace is the filing cabinet for shelf blueprints, and native-thread failure is the loading dock refusing to hire another worker — renting a bigger warehouse changes nothing.

🔁 How to Reproduce Confirm this is your error

Lab captures behind these logs: (1) loop adding byte[1024*1024] to an ArrayList under a tiny lab heap (-Xmx32m) — allocation site lands in main; (2) loop starting sleeping threads with reduced stacks (-Xss512k) until pthread_create returns EAGAIN after ~4000 threads on a laptop. Production traces differ only in the application frames; the header lines match verbatim.

🛠️ Solutions (5 Ways to Fix)

Solution 1✓ Most common cause

Measure live set with jcmd/jmap, then size the heap with headroom

👉 Use this when you must decide between "leak" and "too small" before changing anything.

Trigger a full GC, read the histogram, and compare live set to max heap. If live data sits near the ceiling with no leak, the workload genuinely needs more memory: set -Xms equal to -Xmx (avoids resize churn) and leave roughly double the observed live set. If the histogram shows one type growing forever, it is a leak — sizing only delays the crash.

jcmd <pid> GC.heap_info # capacity vs used jcmd <pid> GC.run # full GC for a clean reading jmap -histo:live <pid> | head -20 # dominant types jcmd <pid> GC.heap_dump /tmp/live.hprof # open in Eclipse MAT # Verdict "needs headroom", live set ~2 GB -> 4 GB heap: java -Xms4g -Xmx4g -XX:+HeapDumpOnOutOfMemoryError -jar app.jar
Solution 2

Make the JVM container-aware (Kubernetes / OpenShift)

👉 Use this when pods get OOMKilled (exit 137) or the heap ignores container limits.

Since Java 10 the VM honors cgroup limits by default (UseContainerSupport). Express the heap as a PERCENTAGE of the container limit instead of absolute gigabytes so the same image works across pod sizes — and always leave room for non-heap pools (metaspace, threads, direct buffers), which is why 75% is the usual ceiling. Distinguish failures: exit 137 means the LINUX kernel killed the container (lower the percentage or raise the limit); java.lang.OutOfMemoryError means the JVM did it internally.

resources: requests: { memory: "2Gi" } limits: { memory: "4Gi" } env: - name: JAVA_TOOL_OPTIONS value: "-XX:MaxRAMPercentage=75.0 -XX:+HeapDumpOnOutOfMemoryError"
Solution 3

Find the leak with Eclipse MAT dominator trees

👉 Use this when repeated heap dumps show one component growing between snapshots.

Open two dumps taken hours apart, diff the classloader trees, and walk the dominator tree: MAT names the single retained-size owner (usually a static Map, an unbounded cache, or a ThreadLocal never removed on pooled threads). Fixing the retention beats every flag — memory leaks are references, not shortages.

# Dump twice, e.g. at t0 and t0+2h, then in MAT: # Leak Suspects Report -> Dominator Tree -> Path to GC Roots jcmd <pid> GC.heap_dump /tmp/t0.hprof jcmd <pid> GC.heap_dump /tmp/t1.hprof # Cheap continuous signal between dumps: jstat -gcutil <pid> 10s # watch Old Gen % climb and Full GCs lengthen
Solution 4

Tell the other OutOfMemoryError variants apart

👉 Use this when the message after the colon is NOT "Java heap space".

Metaspace = class metadata pool full — typical with heavy dynamic proxy/CGLIB generation or classloader leaks in redeploy-heavy apps; check class counts (jcmd GC.class_stats) before raising MaxMetaspaceSize. GC overhead limit exceeded = GC runs almost constantly while reclaiming almost nothing — treat it as a heap-space symptom with extra warning time. unable to create native thread = OS refused pthread_create (EAGAIN): ulimits, container pids limits, or sheer thread count — not a heap problem, so raising -Xmx makes it WORSE.

# Metaspace pressure: who is generating classes? jcmd <pid> GC.class_histogram | head # Thread exhaustion: count and locate jcmd <pid> Thread.print | grep -c "prio=" cat /proc/<pid>/limits | grep processes # Only AFTER confirming genuine metadata need: java -XX:MaxMetaspaceSize=256m -jar app.jar
Solution 5

Size container limits around TOTAL JVM footprint, not just heap

👉 Use this when pods still die with OOMKilled (exit 137) even though MaxRAMPercentage is configured.

MaxRAMPercentage caps the HEAP only. Metaspace, thread stacks, code cache, GC bookkeeping, and direct buffers live outside it and typically add 20-35% resident memory — the kernel kills the container the moment TOTAL usage crosses the limit, regardless of any Java-level flag. Precedence trap: if both -Xmx and MaxRAMPercentage are present, -Xmx silently wins, so configure exactly one mechanism. Measure real totals with NativeMemoryTracking and budget the limit as roughly target-heap divided by 0.7.

# Measure every pool, not just heap: java -XX:NativeMemoryTracking=summary -XX:MaxRAMPercentage=70.0 -jar app.jar & jcmd <pid> VM.native_memory summary | head -40 # Rule of thumb: container limit >= target heap / 0.7 # 3 GB target heap -> 4Gi limit leaves room for metaspace, # thread stacks, code cache, and direct buffers.

📋 Version Notes

Java 8

PermGen is GONE — replaced by Metaspace, so permanent-generation tuning flags are ignored. Container awareness absent: -Xmx must be set explicitly or the heap defaults to 1/4 of machine RAM, a classic cause of OOMKilled pods.

Java 15

UseContainerSupport on by default (since 10, refined through 15+): MaxRAMPercentage reliably caps heap inside cgroup-limited containers.

Java 17

G1 remains the default collector with region-based humongous-object handling; ZGC and Shenandoah are production-ready alternatives for large heaps with latency ceilings.

Java 21

Virtual threads massively lower cost-per-thread but still consume native stacks at scale — unable-to-create-native-thread OOMs shift from thread COUNT toward OS limits (ulimit, cgroup pids.max).

🛡️ How to Prevent This Next Time

Keep HeapDumpOnOutOfMemoryError enabled in every environment, alert on old-gen occupancy trends (jstat or APM) instead of waiting for the crash, bound every cache with a maximum size, and express heap as MaxRAMPercentage rather than absolute -Xmx in containerized deployments.