Spring Boot out of memory error — JVM heap diagnosis
TL;DR
How to diagnose and fix java.lang.OutOfMemoryError in Spring Boot applications with JVM heap tuning and leak analysis.
Key facts
- Topic
- Production error triage
- Stack
- Java / Linux
TL;DR
java.lang.OutOfMemoryError: Java heap space is the most common JVM crash in Spring Boot production environments. The JVM allocates a fixed maximum heap at startup (-Xmx), and once live objects fill that space faster than the garbage collector can reclaim it, the process throws an OOM error and typically becomes unresponsive or terminates.
OOM variants
Not all OutOfMemoryError crashes are the same:
- Java heap space — live objects exceed
-Xmx. The most common case. - Metaspace — class metadata fills the metaspace region. Frequent in apps with heavy reflection, proxying, or hot-reloading. Tune with
-XX:MaxMetaspaceSize. - Direct buffer memory — off-heap
ByteBufferallocations exceed-XX:MaxDirectMemorySize. Common with Netty-based stacks (Spring WebFlux, reactive drivers). - GC overhead limit exceeded — the JVM spends >98% of time in GC while reclaiming <2% of heap. Effectively the same as heap space exhaustion.
Immediate diagnosis
Check if the process was killed by the OS or the JVM itself:
dmesg | grep -i "killed process"
journalctl -u springapp --since "30 minutes ago" | grep -i "outofmemory"
If you configured -XX:+HeapDumpOnOutOfMemoryError, a .hprof file should exist:
ls -lh /opt/springapp/*.hprof
Capture and analyse a heap dump
If no automatic dump exists, capture one from a running (but struggling) process:
jmap -dump:live,format=b,file=/tmp/heap.hprof $(pgrep -f 'springapp')
Open the dump in Eclipse MAT (Memory Analyzer Tool):
- Run the Leak Suspects report — MAT identifies the largest retained object trees
- Check the Dominator Tree for objects holding the most memory
- Look for collections (HashMap, ArrayList) with millions of entries
Common leak patterns
- Unclosed database connections — connection pool exhaustion where each leaked connection holds result set buffers
- Static collections growing indefinitely — in-memory caches without eviction (use Caffeine or Spring Cache with TTL)
- ThreadLocal storage not cleaned — thread pools reuse threads, so ThreadLocal values persist across requests
- Session-scoped beans holding large objects — each user session accumulates data that is never released
- Hibernate first-level cache — long-running transactions accumulate entities in the persistence context
JVM tuning
Set initial and max heap to the same value in production to avoid resize pauses:
java -Xms1g -Xmx1g -XX:+UseG1GC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/opt/springapp/dumps/ \
-Xlog:gc*:file=/var/log/springapp/gc.log:time,uptime:filecount=5,filesize=20m \
-jar app.jar
Analyse GC logs to determine if the heap is genuinely too small or if a leak is consuming it:
# Quick GC log summary — look for full GC frequency and pause times
grep "Full GC" /var/log/springapp/gc.log | tail -20
If full GCs happen every few seconds and reclaim almost nothing, you have a leak. If they happen rarely but the heap is simply too small for the workload, increase -Xmx.
Where Reflex helps
Reflex monitors JVM heap utilisation via Spring Boot Actuator metrics and system-level RSS tracking. When heap usage crosses a configurable threshold, Reflex can trigger a graceful restart via systemd before the OOM error fires, capture diagnostic data, and alert your team with a timeline of memory growth correlated with recent deployments. See How it works.