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

The memory areas of a Java process and the OutOfMemoryError of each one: unable to create native thread for the thread stacks, Java heap space and GC overhead limit exceeded for the heap, Metaspace for the Metaspace, Cannot reserve … bytes of direct buffer memory for the direct memoryJava processeach message tells you which area is fullThread stacksone system thread for eachnew Thread · -Xsslimit: the systemunable to createnative threadHeapshared · managed by the GC-XmxJava heap spaceGC overhead limitexceededParallel; G1 since 25.0.3Metaspacenative memory-XX:MaxMetaspaceSizeMetaspaceDirect memoryByteBuffer.allocateDirect()·-XX:MaxDirectMemorySizeCannot reserve … bytes of direct buffer memory (allocated: …, limit: …)on Java 17, 21 and 25 · outside the heap · Java 8 and 11: Direct buffer memory
MessageFull areaFirst check
Java heap spacethe heapthe heap after each GC (-Xlog:gc*, jcmd <pid> GC.heap_info)
GC overhead limit exceededthe heap, almost full all the timethe same: the GC runs nonstop
Metaspacethe class metadatajcmd <pid> VM.metaspace
Cannot reserve … bytes of direct buffer memorythe direct memory-XX:MaxDirectMemorySize, jcmd <pid> VM.native_memory
unable to create native threadthe threads of the systemjcmd <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:

-XmxJava 8Java 11Java 17Java 21Java 25
16mGC overheadJava heap spaceJava heap spaceJava heap spaceJava heap space
32mno error after 60 sGC overheadGC overheadGC overheadno error after 60 s
128mno error after 60 sGC overheadGC overheadGC overheadJava 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

  1. Read the message: it tells you which area is full (table at the top).
  2. Get a heap dump: with -XX:+HeapDumpOnOutOfMemoryError, the JVM writes an .hprof file. In my tests, it is written for Java heap space, GC overhead limit exceeded and Metaspace, but not for direct buffers. Keep some disk space free: the file can be several GB.
  3. Look at the heap after each GC: stable, the heap is too small; going up, it is a leak (see below).
  4. 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 readWhat is true todaySince
OutOfMemoryError: Direct buffer memoryCannot reserve … bytes of direct buffer memory (allocated: …, limit: …)Java 17 in my tests (11 still had the old one)
unable to create new native threadunable to create native thread: possibly out of memory or process/resource limits reachedJava 11 in my tests (8 had the old one)
GC overhead limit exceeded comes only from ParallelG1 can throw it tooJava 26, and Java 25.0.3 as a backport
PermGen spaceMetaspaceJava 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).

Further reading