1. Read the message
Search the log (journalctl -u yourservice, catalina.out or the application log) for OutOfMemoryError. The text after the colon decides everything:
| Message | Meaning | Fix |
|---|---|---|
Java heap space | Objects do not fit in the heap | Raise -Xmx or find the leak |
GC overhead limit exceeded | The JVM spends almost all its time in GC and frees almost nothing | Same as heap space |
Metaspace | Too many loaded classes, often from redeploys | Raise MaxMetaspaceSize, restart instead of redeploying |
unable to create native thread | Process or OS thread limit, or no memory for stacks | Fewer threads, lower -Xss, higher limits |
Direct buffer memory | Off-heap buffers (Netty, NIO) | Raise -XX:MaxDirectMemorySize |
2. Is it the heap or the server?
If the log has no OutOfMemoryError but the service just disappeared, the Linux kernel killed it because the whole server ran out of RAM. Check:
sudo dmesg -T | grep -i -E "out of memory|killed process"
sudo journalctl -k | grep -i "killed process"
free -m
That is not a heap problem. The heap plus metaspace, thread stacks, the database and the OS exceed physical memory. Lower -Xmx (a bigger heap is often the wrong answer here), stop other memory users, or move to a bigger server. See how much RAM a Java app needs.
3. Heap space: leak or too small?
Turn on logging of garbage collections and a dump on failure. Add to the JVM options:
-Xlog:gc*:file=/var/log/myapp/gc.log:time,uptime:filecount=5,filesize=10m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/tmp/myapp.hprof
-XX:+ExitOnOutOfMemoryError
Create the log folder and give the service user write access. Then read the log. If the heap after each full GC stays flat and the application is simply busy, the heap is too small: raise it. If the heap after full GC climbs over hours or days, that is a leak: no heap size will fix it, only delay it.
ExitOnOutOfMemoryError makes the JVM exit so systemd can restart it cleanly. A JVM that has thrown OOM is unreliable and best replaced.
4. Find the leak
Open the .hprof file in Eclipse Memory Analyzer (MAT) on your computer, not on the server. Run the Leak Suspects report. It shows the largest object trees and what holds them. Typical culprits: an unbounded cache or static map, sessions that never expire, a result set loaded entirely into memory, a thread local that is never cleared.
A quick look without a dump:
sudo -u appuser jcmd <pid> GC.class_histogram | head -20
Run it twice, a few minutes apart, and compare the leading classes.
5. Metaspace
Normal applications settle at 100 to 300 MB. Growth that never stops usually means a classloader leak: repeated hot redeploys in Tomcat or WildFly are the classic cause. The fix is to restart the service on deploy. Set a ceiling so it fails visibly instead of eating RAM: -XX:MaxMetaspaceSize=256m.
6. Cannot create native thread
ulimit -u # user process limit
cat /proc/<pid>/limits | grep -i processes
ps -o nlwp= -p <pid> # threads of your JVM
If the thread count is in the thousands, look for an unbounded executor or a thread per request. Set TasksMax=infinity and a larger LimitNPROC in the systemd unit only if you really need that many threads.
What not to do
Do not set -Xmx to nearly all the RAM hoping it goes away. The heap is only part of the process, and you will trade a Java error for the kernel killing the process. Size the server instead: the JVM heap calculator gives a safe starting point.
Last updated October 29, 2026.