> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Java OutOfMemoryError: Every Message and What It Means
- URL: https://www.ggorantala.dev/java-outofmemoryerror-types/
- Published: 2026-09-02T13:47:00.000Z
- Updated: 2026-10-30T14:53:22.000Z
- Description: Java heap space, Metaspace, GC overhead, direct buffer, native thread: every OutOfMemoryError message, the memory area behind it, and where to look first.
- Author: Gopi Gorantala
- Tags: java-memory-management, outofmemoryerror, troubleshooting

The most useless OutOfMemoryError I've ever been handed was this one:

```log
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

```

No detail message. No stack trace. Nothing after the colon to tell you which of the several different things called a Java OutOfMemoryError just happened. I got it on purpose, in a lab, on JDK 21.0.12.1 with `-Xmx64m`, and the section on the bystander thread below shows how. In production you get it at 3 a.m. and someone pastes it into the incident channel with "heap?".

Maybe. Maybe not. `OutOfMemoryError` is one class with at least eight different jobs, and the text after the colon is the only thing that says which job. Heap, Metaspace, thread stacks, direct buffers and the kernel all run out in their own way, and each wants a different fix. This article is the map. Every message I could reproduce is below with the output that came out of my run, and every one links to the article that goes the full distance.

## Find out what actually failed, message by message

Read the text after `java.lang.OutOfMemoryError:`. That's the whole triage. Here's the table I keep open during an incident. "Verified" means I made the JVM print that exact string this session; the rest I'm taking from the HotSpot source and the Troubleshooting Guide, and I've marked them.

| Message                                                                                   | What ran out                                                  | Who threw it                                       | First thing I check                                  | Deep dive                                                                                              |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| Java heap space                                                                           | Java heap, full of live objects                               | JVM, at allocation                                 | Heap after GC over time: flat, climbing, or spiking? | [heap space](https://ggorantala.dev/outofmemoryerror-java-heap-space/?ref=ggorantala.dev)              |
| GC overhead limit exceeded                                                                | Heap again, but the JVM gave up early                         | JVM (Parallel GC in my run)                        | Same as above, plus which collector you run          | [overhead limit](https://ggorantala.dev/gc-overhead-limit-exceeded/?ref=ggorantala.dev)                |
| Metaspace                                                                                 | Class metadata (native memory)                                | JVM                                                | Loaded class count over time; redeploys              | [Metaspace](https://ggorantala.dev/outofmemoryerror-metaspace/?ref=ggorantala.dev)                     |
| Compressed class space                                                                    | The slice of class memory that compressed class pointers need | JVM                                                | jcmd <pid> VM.metaspace                              | [class space](https://ggorantala.dev/outofmemoryerror-compressed-class-space/?ref=ggorantala.dev)      |
| Requested array size exceeds VM limit                                                     | Nothing. The array length is over the maximum                 | JVM                                                | Where one array grows by doubling                    | [array limit](https://ggorantala.dev/requested-array-size-exceeds-vm-limit/?ref=ggorantala.dev)        |
| unable to create native thread: possibly out of memory or process/resource limits reached | OS threads, pids, or stack address space                      | JVM, after pthread\_create fails                   | Thread count vs ulimit -u and the cgroup pids limit  | [native thread](https://ggorantala.dev/unable-to-create-native-thread/?ref=ggorantala.dev)             |
| Cannot reserve N bytes of direct buffer memory (allocated: A, limit: L)                   | The MaxDirectMemorySize budget                                | JDK library (java.nio.Bits)                        | BufferPoolMXBean usage                               | [direct buffers](https://ggorantala.dev/outofmemoryerror-direct-buffer-memory/?ref=ggorantala.dev)     |
| Map failed (wording varies; not reproduced here)                                          | Memory-mapped regions                                         | JDK library                                        | vm.max\_map\_count                                   | [mmap](https://ggorantala.dev/java-map-failed-mmap-limit/?ref=ggorantala.dev)                          |
| Out of swap space? / "insufficient memory" (not reproduced here)                          | Native malloc or mmap failed                                  | JVM, usually as a fatal crash with an hs\_err file | The hs\_err\_pid file header                         | [native alloc failure](https://ggorantala.dev/jvm-insufficient-memory-out-of-swap/?ref=ggorantala.dev) |

One more entry that isn't in the table because it isn't Java's at all: exit code 137 and a pod showing `OOMKilled`. There's no exception, no stack trace, no heap dump, because the JVM never got a say. The kernel killed the process. That's [a different article](https://ggorantala.dev/java-oomkilled-exit-code-137/?ref=ggorantala.dev), and [this comparison](https://ggorantala.dev/outofmemoryerror-vs-oomkilled/?ref=ggorantala.dev) is how to tell the two apart quickly.

Notice who threw each one. Most come from the JVM, but the direct buffer one is thrown by plain Java code in `java.nio.Bits.reserveMemory`, and my trace shows it. That matters when you hunt for a cause, because the JVM didn't hit a wall at all. A counter in a library crossed a configured limit.

Two commands narrow things down further, and both need the JVM alive:

```bash
jcmd <pid> GC.heap_info                    # per-space heap occupancy
jcmd <pid> Thread.print | grep -c '^"'     # rough live thread count

```

If you're past the point of having a live JVM, the message in the log is what you've got. Which is why the next section matters.

## Break it on purpose

I wrote one small program with a mode switch and ran each mode in its own JVM, so every message below is real output. The setup: OpenJDK 21.0.12.1 on x86\_64 Linux, 2 cores, 8 GB, no container (I had no Docker daemon and no Kubernetes available, so nothing here is container-specific). All of it is bounded with small limits. Run it that way too, never on a shared box, and put the thread one in a throwaway container.

```java
import java.nio.ByteBuffer;
import java.util.*;

public class OomLab {
    static final List<Object> keep = new ArrayList<>();
    public static void main(String[] a) throws Exception {
        switch (a[0]) {
            case "heap"    -> { while (true) keep.add(new byte[1024 * 1024]); }          // live data, GC can't reclaim it
            case "direct"  -> { while (true) keep.add(ByteBuffer.allocateDirect(1024 * 1024)); }
            case "threads" -> { while (true) { Thread t = new Thread(() -> { try { Thread.sleep(600_000); } catch (InterruptedException e) {} }); t.start(); keep.add(t); } }
            case "maxarr"  -> {
                for (int n : new int[]{Integer.MAX_VALUE, Integer.MAX_VALUE - 1, Integer.MAX_VALUE - 2}) {
                    try { byte[] b = new byte[n]; System.out.println(n + " ok"); } catch (Throwable t) { System.out.println(n + " -> " + t); }
                }
            }
        }
    }
}

```

(The `meta` mode defines empty classes through a fresh class loader in a loop and keeps them reachable. It's about forty lines of hand-built class-file bytes, so I've left it out; any class-generating loop does the same job.)

Commands and what they printed:

```log
$ java -Xmx64m OomLab direct   (with -XX:MaxDirectMemorySize=16m)
Exception in thread "main" java.lang.OutOfMemoryError: Cannot reserve 1048576 bytes of direct buffer memory (allocated: 16777216, limit: 16777216)
	at java.base/java.nio.Bits.reserveMemory(Bits.java:178)

$ java -XX:MaxMetaspaceSize=16m OomLab meta
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.base/java.lang.ClassLoader.defineClass1(Native Method)

$ java -XX:MaxMetaspaceSize=256m -XX:CompressedClassSpaceSize=1m OomLab meta
Exception in thread "main" java.lang.OutOfMemoryError: Compressed class space

$ java -Xmx64m -XX:+UseParallelGC OomLab overhead     (a HashMap of tiny entries)
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

$ java -Xmx64m -XX:+UseG1GC OomLab overhead           (same code, different collector)
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

```

That last pair is the first surprise, and I'll come back to it. The thread one needs a user the kernel will actually limit (root bypasses `RLIMIT_NPROC`), so I ran it as `nobody` with `ulimit -u 200` inside a subshell:

```log
[0.055s][warning][os,thread] Failed to start thread "Unknown thread" - pthread_create failed (EAGAIN) for attributes: stacksize: 256k, guardsize: 0k, detached.
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
	at java.base/java.lang.Thread.start0(Native Method)

```

Heap was nowhere near full there. About 180 threads were all it took under that limit. And afterwards the JVM did not exit. The 180 sleeping non-daemon threads kept it alive with `main` already dead, and I had to `kill -9` it (my `timeout -s KILL` did, hence the 137 in my own log, which is the shell's, not a container's). Keep that in mind. An OOM in one thread doesn't end a process.

Teardown: nothing in that set leaves files except heap dumps, if you add `-XX:+HeapDumpOnOutOfMemoryError`. A 64 MB heap gave me a 68,643,638-byte dump in 0.05 seconds, which is cute at lab scale and a different story at 14 GB.

## What the message is really telling you

The detail message says which resource hit its limit. It doesn't say why. And it doesn't say that the thing that threw is the thing that caused it.

That last part bit me in the lab. I set up one thread that hoards 512 KB arrays and another that does trivial work with the odd string. With `-Xmx64m`, the first line of output was `java.lang.OutOfMemoryError: Java heap space` from the heap-dump hook, then two lines saying the OOM was "thrown from the UncaughtExceptionHandler" in the thread named `bystander` and in `leaker`. The order of the two lines flipped between runs. In all three runs both threads got the error, the guilty one and the innocent one, because any thread that asks for memory at that moment gets it.

Back to that mystery line at the top. The default handler tried to print a stack trace, which allocates, in a heap that was already full, so it failed too. What you get is the one-line message with nothing in it. I didn't take this further than the observation. When the same program caught the error itself with a little headroom left, it printed a normal one-frame trace, so the handler failing is about headroom at that instant. When you see a detail-free OOM, look for `-XX:+HeapDumpOnOutOfMemoryError` output or GC logs, because the log line won't help.

## How the JVM ends up throwing a Java OutOfMemoryError

I went looking for the throw site because I wanted to know how often a full heap just means "full". In `memAllocator.cpp`, `MemAllocator::Allocation::check_out_of_memory` picks the string from one flag: `"GC overhead limit exceeded"` if the overhead flag was set, `"Java heap space"` otherwise, then throws a pre-built exception object from `Universe` (the source calls accessors like `out_of_memory_error_java_heap()`; I read this at the `jdk-21+35` tag, and I'm inferring "pre-built" from the accessor names and from the zero-frame stack I measured on the bystander thread). That explains the stackless errors. The JVM can't count on having memory to build a trace in.

What leads up to that line is the allocation path: the thread's allocation buffer is full, the heap can't expand, the JVM runs collections and retries, and only then does it give up. Heap layout and collectors own that story, and [the JVM series covers it](https://ggorantala.dev/java-heap-eden-survivor-old-gen/?ref=ggorantala.dev). The piece I care about here is that heap OOM is a result, not a diagnosis.

Then the overhead limit. Same Parallel-versus-G1 code, two different messages. `GC overhead limit exceeded` showed up with `-XX:+UseParallelGC`, and G1 on the same JDK and same heap printed plain `Java heap space`. That's one JDK version on one machine; I did not confirm in source which collectors enforce the limit on JDK 22+ or 25, so don't read it as universal. The full treatment is [planned for its own article](https://ggorantala.dev/gc-overhead-limit-exceeded/?ref=ggorantala.dev), and there's [an older post on this site](https://ggorantala.dev/java-outofmemoryerror-java-heap-space-gc-overhead-limit/?ref=ggorantala.dev) that covers heap space and the overhead limit together.

The second surprise was the array one. I'd always said "arrays top out near `Integer.MAX_VALUE`". Here's what a 2 GB heap actually did:

```log
2147483647 -> java.lang.OutOfMemoryError: Requested array size exceeds VM limit
2147483646 -> java.lang.OutOfMemoryError: Requested array size exceeds VM limit
2147483645 -> java.lang.OutOfMemoryError: Java heap space

```

Two below `Integer.MAX_VALUE` is where the limit stops being a limit and goes back to being a heap question. Nothing about free memory is involved in the first two. That one message has nothing to do with the leak you're hunting.

## Fixing it properly, and the rule about adding RAM

My rule: **don't raise a limit until you've ruled out growth.** Raising `-Xmx`, `-XX:MaxMetaspaceSize` or `-XX:MaxDirectMemorySize` gets you a longer time to failure, and sometimes that's exactly the wrong thing, because it makes the failure rarer and harder to reproduce.

The test is the same for every area that holds long-lived data. Graph the resource after the cleaner has run (heap after GC; loaded class count; direct buffer pool usage; live threads) over hours or days. Three shapes:

- **Flat** with spikes: the limit is too tight. Raising it is fine.
- **Climbing, never returning**: something's holding on. Raising the limit buys time, nothing else.
- **A step that appears and stays** after a deploy or a traffic change: you changed the live set, or you leaked once. Compare before and after.

Tools for the climbing case live in the leak articles: [heap dumps](https://ggorantala.dev/java-memory-leak-heap-dump/?ref=ggorantala.dev), [setting up the dump itself](https://ggorantala.dev/heapdumponoutofmemoryerror/?ref=ggorantala.dev), and [class loader leaks](https://ggorantala.dev/java-classloader-leak/?ref=ggorantala.dev) for the Metaspace cases. These URLs go live as the series publishes them.

Fixes that aren't fixes: disabling the overhead check, which turns a clear error into a JVM that spends nearly all its time in GC before dying anyway; `-Xss` bumps for a thread-count problem (bigger stacks mean fewer threads fit); and a nightly restart as a cure. A restart schedule is a monitoring decision wearing a fix's clothes.

## What I'd do instead of tuning limits

Bound everything that can grow, by default. Caches with a maximum size. Queues with a capacity. Thread pools with a fixed ceiling, not `newCachedThreadPool` in a request path. Streaming instead of reading whole files into memory. One owner for every resource that has to be closed.

And run production with `-XX:+HeapDumpOnOutOfMemoryError` pointed at a volume that survives the container, and with `-XX:+ExitOnOutOfMemoryError` so the process dies instead of limping along half-broken. After the bystander experiment I'm even more convinced of the second one. A pool whose workers died of an OOM looks perfectly healthy to a liveness probe that only checks the port. (Exit behaviour for OOMs thrown from library code, like the direct buffer one, I haven't checked, so test yours.)

## Which JDK versions behave how

Everything above ran on OpenJDK 21.0.12.1\. Things I know changed or that I'd re-check on an upgrade:

- The direct buffer message in my run reads `Cannot reserve 1048576 bytes of direct buffer memory (allocated: 16777216, limit: 16777216)`. Older JDKs printed just `Direct buffer memory`. If you grep logs across fleets, match both.
- The native thread message on 21 ends with `possibly out of memory or process/resource limits reached`. Search for the prefix.
- `PermGen space` is gone since JDK 8\. If an old runbook mentions `MaxPermSize`, the Metaspace flags replaced it.
- Which collectors enforce the overhead limit: I tested two on one release. Test your own collector on your own JDK.
- The JDK landscape page I fetched on openjdk.org listed JDK 25 as the latest GA and 26 and 27 as in development, but that page looked stale for an October 2026 date, so I haven't checked 25 or 26 behaviour above.

## Screenshot this bit

- The text after `OutOfMemoryError:` is the diagnosis. If it's empty, get the heap dump or the GC log.
- The thread that threw is not necessarily the thread that caused it. Mine killed the innocent one about as often.
- `Requested array size exceeds VM limit` is about length, not free memory. Two below `Integer.MAX_VALUE` it turns into a normal heap error.
- Graph before you raise. A climbing line after GC is a leak and a bigger limit just delays it.
- No exception and exit code 137 means the kernel did it, not Java.

## Questions people paste into search

**What causes java.lang.OutOfMemoryError?**  
A request for memory that the JVM or a JDK class can't satisfy. The message names the resource: heap, Metaspace, direct buffers, native threads, or an array length over the limit. See the table above.

**Is increasing -Xmx a good fix for OutOfMemoryError: Java heap space?**  
Only if heap after GC is flat and just too close to the limit. If it climbs and doesn't come back down, you have a leak and a bigger heap just delays the error. [The heap space article](https://ggorantala.dev/outofmemoryerror-java-heap-space/?ref=ggorantala.dev) has the triage.

**Can I catch an OutOfMemoryError?**  
You can, but the JVM may not have the memory to run your handler. In my lab, even printing the error failed. Catching is for narrow cases such as rejecting one oversized request; it isn't a recovery plan.

**How is OOMKilled different from OutOfMemoryError?**  
OutOfMemoryError is thrown by the JVM or the JDK and leaves a trace. OOMKilled is the kernel killing the process for exceeding its cgroup memory limit, with exit code 137 and no Java output. See [OutOfMemoryError vs OOMKilled](https://ggorantala.dev/outofmemoryerror-vs-oomkilled/?ref=ggorantala.dev).