An OutOfMemoryError is not one single problem. The text after the
colon tells you which memory area is full: the heap, the Metaspace,
the direct memory or the threads. And each area is checked and fixed
in a different way.
For each message, I wrote a small program that causes it. They are in
the public repository
java-gc-lab,
one file per message, to run with java without compiling first (Java
11 or later).
The message tells you which area is full
| Message | Full area | First check |
|---|---|---|
Java heap space | the heap | the heap after each GC (-Xlog:gc*, jcmd <pid> GC.heap_info) |
GC overhead limit exceeded | the heap, almost full all the time | the same: the GC runs nonstop |
Metaspace | the class metadata | jcmd <pid> VM.metaspace |
Cannot reserve … bytes of direct buffer memory | the direct memory | -XX:MaxDirectMemorySize, jcmd <pid> VM.native_memory |
unable to create native thread | the threads of the system | jcmd <pid> Thread.print, the system limits |
Java heap space
java -Xmx64m oom/HeapSpace.java
java.lang.OutOfMemoryError: Java heap space
The program keeps 1 MB arrays in a list: the GC cannot free anything, and the 64 MB heap is full in less than a second.
Oracle’s guide is clear: this message does not always mean a memory leak. The heap can simply be too small for the application. But in an application that runs for a long time, it is often the sign that it keeps references to objects without meaning to.
To know which one it is, look at the heap after each GC: with
-Xlog:gc*, or with jcmd <pid> GC.heap_info on a running
application. If it always goes back to the same level, the heap is just
too small. If it goes up from one GC to the next, something keeps the
objects.
GC overhead limit exceeded
java -Xmx32m -XX:+UseParallelGC oom/GcOverhead.java
java.lang.OutOfMemoryError: GC overhead limit exceeded
Here, the heap fills up slowly, with small objects. The GC runs nonstop and frees almost nothing. The JVM then prefers to stop rather than go on without making progress: according to the docs, it throws this error if more than 98% of the time goes to the GC and less than 2% of the heap is recovered, for five GCs in a row.
This message is not guaranteed. With the same program and the Parallel GC, here is what I get:
-Xmx | Java 8 | Java 11 | Java 17 | Java 21 | Java 25 |
|---|---|---|---|---|---|
| 16m | GC overhead | Java heap space | Java heap space | Java heap space | Java heap space |
| 32m | no error after 60 s | GC overhead | GC overhead | GC overhead | no error after 60 s |
| 128m | no error after 60 s | GC overhead | GC overhead | GC overhead | Java heap space |
“No error after 60 s” is the worst case: the JVM spends its time in the GC and the application does nothing, with no error.
For a long time, only Parallel threw this error. G1, the default GC,
has it too since Java 26, with a backport in Java 25.0.3
(JDK-8212084). But in my
tests with G1 on Java 25.0.4, from -Xmx16m to -Xmx256m, I always
get Java heap space. According to the guide, the usual cause is the
same: live data that barely fits in the heap.
Metaspace
java -XX:MaxMetaspaceSize=32m oom/MetaspaceFull.java
java.lang.OutOfMemoryError: Metaspace
The Metaspace holds the class metadata, in native memory. The program creates a new class loader and a new class in it, again and again; since it keeps them, nothing can be unloaded.
By default, the Metaspace has no limit. If a limit is set with
-XX:MaxMetaspaceSize, the guide advises to raise it. If the Metaspace
keeps growing (jcmd <pid> VM.metaspace), the problem is more likely
classes that cannot be unloaded, as in this program.
Direct buffer memory
java -XX:MaxDirectMemorySize=32m oom/DirectBuffer.java
java.lang.OutOfMemoryError: Cannot reserve 1048576 bytes of direct buffer memory (allocated: 32514048, limit: 33554432)
Direct buffers (ByteBuffer.allocateDirect) live outside the heap.
Their limit is -XX:MaxDirectMemorySize; without this option, the JVM
chooses it.
Before Java 17: on Java 8 and 11, the message was just
Direct buffer memory. Since Java 17, in my tests, it gives the size asked for, the memory already taken and the limit, which helps a lot.
To see this memory on a running application:
-XX:NativeMemoryTracking=summary at startup, then
jcmd <pid> VM.native_memory.
Unable to create native thread
This program starts threads until the system says no. Do not run it on your machine without a limit: I ran it in a container limited to 200 processes.
docker run --rm --pids-limit 200 -v "$PWD/oom":/oom eclipse-temurin:25 java /oom/NativeThreads.java
java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
Before Java 11: on Java 8, the message was
unable to create new native thread.
Each thread started with new Thread is a thread of the operating
system, with its own stack outside the heap. So the cause is a limit of
the system (number of processes or threads, memory), or an application
that creates threads without end, for example a pool with no maximum
size. jcmd <pid> Thread.print shows all the threads, with their stack
traces. Raising -Xmx does not change a system limit.
The method, whatever the message
- Read the message: it tells you which area is full (table at the top).
- Get a heap dump: with
-XX:+HeapDumpOnOutOfMemoryError, the JVM writes an.hproffile. In my tests, it is written forJava heap space,GC overhead limit exceededandMetaspace, but not for direct buffers. Keep some disk space free: the file can be several GB. - Look at the heap after each GC: stable, the heap is too small; going up, it is a leak (see below).
- Fix: give more room to the right area, or find what keeps the objects.
Heap too small, or a leak?
With -Xlog:gc, each GC writes one line. The number that matters is
the one after the arrow: the heap after the GC, so roughly the data
that is still alive.
With SteadyLoad.java, the live data does not change. After each GC, the heap goes back to the same level:
java -Xmx64m -Xlog:gc oom/SteadyLoad.java
GC(2) Pause Young (Normal) (G1 Evacuation Pause) 41M->24M(64M) 0.379ms
GC(5) Pause Young (Normal) (G1 Evacuation Pause) 47M->24M(64M) 0.538ms
GC(9) Pause Young (Normal) (G1 Evacuation Pause) 54M->24M(64M) 0.624ms
With SlowLeak.java,
the program keeps 1 MB more in each round. After each GC, the heap goes
up, until Java heap space:
java -Xmx64m -Xlog:gc oom/SlowLeak.java
GC(1) Pause Young (Normal) (G1 Evacuation Pause) 30M->5M(64M) 2.292ms
GC(4) Pause Young (Normal) (G1 Evacuation Pause) 42M->11M(64M) 0.916ms
GC(8) Pause Young (Normal) (G1 Evacuation Pause) 50M->19M(64M) 0.938ms
GC(12) Pause Young (Normal) (G1 Evacuation Pause) 54M->29M(64M) 1.283ms
On an application that is already running, without a restart,
jstat -gcutil <pid> 1s prints the use of each area every second: in
the second case, the O column (the old generation, in %) goes up the
same way. Its docs say that it is experimental and unsupported.
Raising -Xmx is not a mistake in itself: it is even the first action
that Oracle’s guide gives for Java heap space. The mistake is to
raise it again and again when the heap goes up after each GC: the leak
is still there, it just takes longer to fill the heap.
What has changed
| What you still read | What is true today | Since |
|---|---|---|
OutOfMemoryError: Direct buffer memory | Cannot reserve … bytes of direct buffer memory (allocated: …, limit: …) | Java 17 in my tests (11 still had the old one) |
unable to create new native thread | unable to create native thread: possibly out of memory or process/resource limits reached | Java 11 in my tests (8 had the old one) |
GC overhead limit exceeded comes only from Parallel | G1 can throw it too | Java 26, and Java 25.0.3 as a backport |
PermGen space | Metaspace | Java 8 |
Tested with Java 8 (Corretto 8u504), 11, 17, 21 and 25 (Temurin
11.0.32, 17.0.20, 21.0.9, 25.0.4), on macOS; the thread case in Docker
(eclipse-temurin).
Sources:
Oracle’s troubleshooting guide on memory leaks,
the documentation of the
java,
jcmd
and jstat
commands (JDK 25), JDK-8212084
(G1 and the overhead limit).