java.lang.OutOfMemoryError: Java heap space — Complete Fix Guide
You've probably been there: an alarm fires in the middle of the night and production logs show java.lang.OutOfMemoryError: Java heap space. This post is a copy-paste-ready, command-first playbook covering cause branching, heap-dump collection and analysis, JVM tuning, and recurrence prevention—finishable in 30 minutes.
Start with the message: OOM cause branching table
OutOfMemoryError is not one error. The text after the colon is the start of the diagnosis. Read that message exactly and branch from it.
| Message | Meaning | Typical causes | First response |
|---|---|---|---|
Java heap space | Not enough heap space to allocate objects | Object accumulation/leaks (unbounded collections), heap simply too small, large queries | Collect a heap dump → MAT analysis, check -Xmx |
GC overhead limit exceeded | GC spends 98%+ of time but reclaims less than 2% of the heap | Heap is nearly full; GC is spinning (often a leak precursor) | Heap dump is mandatory; suspect a leak first |
Metaspace | Class metadata space exhausted | Excessive dynamic class loading, classloader leaks, repeated hot reloads | Check -XX:MaxMetaspaceSize, inspect loaded class count |
Key point: Java heap space and GC overhead limit exceeded are siblings from the same root (heap exhaustion). If you see the latter, treat a leak as more likely.
Collecting a heap dump: automatic + manual commands
Automatic collection (required in production)
These flags dump the heap at the moment OOM occurs. Put them on every production JVM ahead of time. You will regret skipping this.
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/ \
-XX:+ExitOnOutOfMemoryError # prevent zombie state after OOM; force a restartManual collection (from a live process)
When a running process is already in trouble, dump it yourself. Confirm <PID> with jps -l.
# jmap (live dumps only objects that survive GC → smaller dump)
jmap -dump:live,format=b,file=/tmp/heap.hprof <PID>
# jcmd (JDK-recommended, more stable)
jcmd <PID> GC.heap_dump /tmp/heap.hprofQuick occupancy check without a full dump
Before taking a multi-GB dump, you can quickly see which classes are eating memory.
jmap -histo:live <PID> | head -30
jcmd <PID> GC.class_histogram | head -30If byte[], char[], java.util.HashMap$Node, and your domain objects dominate the top of the list in abnormal numbers, that is almost certainly a leak signal.
Finding the leak culprit with Eclipse MAT
Open the collected .hprof in Eclipse MAT. The analysis sequence is always the same.
- Load the hprof → MAT indexes automatically and asks whether to generate a "Leak Suspects Report". Click Yes.
- Leak Suspects report → MAT presents suspected objects as "Problem Suspect 1" with a pie chart. This is where the answer shows up about 80% of the time.
- Dominator Tree → Sort descending by
Retained Heap(total memory that would be reclaimed if that object disappeared). Expand the top object and trace who is holding it. - Identify leak patterns → The usual suspects:
- Growing
staticcacheHashMap/List - Uncleared
ThreadLocal(fatal in thread-pool environments) - Listeners/callbacks registered but never unregistered
- Unbounded accumulation of session/connection objects
- Growing
Right-click the suspect → Path to GC Roots → exclude weak/soft references. That reveals the reference path explaining why the object is not being GC'd.
JVM and container memory tuning
Bare metal / VM
-Xms4g -Xmx4g # Xms = Xmx recommended
-XX:MaxMetaspaceSize=512m
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filecount=5,filesize=10m- Start
-Xmxat 50–70% of physical RAM. The rest is for the OS, metaspace, thread stacks, and direct buffers. - Keep
-Xmsequal to-Xmxso you avoid OS reallocations and full-GC overhead from runtime heap expansion, and so memory is reserved from the start for more predictable behavior.
Docker / Kubernetes
On JDK 17 and 21 LTS, UseContainerSupport is on by default, so the JVM sees cgroup memory limits. In containers, a percentage is safer than a fixed -Xmx.
-XX:MaxRAMPercentage=75.0 # 75% of the container memory limit as heap
-XX:InitialRAMPercentage=75.0⚠️ Pitfall: Older JDKs (before 8u191, etc.) do not see cgroups and size the heap against the entire host memory. The process then exceeds the container limit and dies with OOMKilled. On old versions, set -XX:+UseContainerSupport explicitly, or if that still fails, pin -Xmx yourself.
Leak vs. simply too small — a checklist
This distinction is basically everything. In GC logs (-Xlog:gc*), look at Old Gen usage right after a Full GC.
- ✅ Simply too small: Old Gen drops after GC and only spikes at traffic peaks → raise
-Xmx. - 🚨 Memory leak: Even after GC, Old Gen stair-steps upward and is never reclaimed → growing the heap only buys time; it will die again. Fix the code after MAT analysis.
A note from the field
More than half of production OOMs are not "the heap was too small" but "it never shrank." So in an incident, capture a heap dump first rather than immediately raising -Xmx. A restart destroys the evidence. Dump → restart → MAT analysis is the order that stops you from repeating the same outage.
Recurrence prevention boils down to three things.
- Always collect GC logs (
-Xlog:gc*) and periodically check Old Gen trends - Visualize heap usage with an APM (Pinpoint, Scouter, Datadog, etc.)
- Alert on a heap-usage threshold (e.g. 85%) so you act before OOM
This is heap OOM, not OOMKilled (137)
This is the most common mix-up. This post covers JVM-internal heap (application-level) OOM. Diagnosis starts from java.lang.OutOfMemoryError in the application log.
If the process dies with exit code 137 and there is no heap OOM log at all, that is OOMKilled: the kernel's OOM Killer killed the process for exceeding the container cgroup memory limit. Cause, diagnosis, and fix are completely different (container memory limit, native memory outside the heap, etc.). See a separate OOMKilled diagnosis post for that case.
| Distinction | Java heap space (this post) | OOMKilled (exit 137) |
|---|---|---|
| Actor | JVM | Linux kernel |
| Signal | OOM stack trace in app logs | Container exit, code 137 |
| Fix | Heap dump, code fix, -Xmx | Container limit, native memory |
References: official docs
The primary source for the behavior, flags, and errors in this post is the following official documentation. Check version-specific options and exact behavior there.
FAQ
Q. Does taking a heap dump freeze the service?
A. jmap -dump:live and jcmd GC.heap_dump trigger a STW (Stop-The-World) pause, so the app can freeze for seconds to tens of seconds depending on dump size. Take dumps during low traffic, or in production prefer -XX:+HeapDumpOnOutOfMemoryError so collection happens only at the OOM moment.
Q. Will raising -Xmx alone fix OOM?
A. If the heap is simply too small, yes. If it is a leak, you are only buying time. If Old Gen keeps trending up after GC, it is a code-level leak and MAT analysis is required.
Q. The container has enough memory, but we still get OOM.
A. Check two things. (1) An old JVM that does not see cgroups sized the heap against host memory. (2) -Xmx is fine, but metaspace/direct buffers and other off-heap areas exceeded the container limit and the process died with 137. The latter is OOMKilled, not heap OOM.
Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.
Comments
Be the first to comment.