The java.lang.OutOfMemoryError is the error the JVM throws when it cannot allocate memory for a new object, class, thread or buffer, even after a full garbage collection. The detail message after the class name, such as Java heap space or Metaspace, tells us which memory area ran out, and each area has its own fix.
Most cases come from three causes, an app that loads too much data at once, a memory leak that keeps objects reachable, or a JVM limit that is too small for the real workload. The following example triggers two of the messages on purpose and reads the heap limit.
long maxHeap = Runtime.getRuntime().maxMemory(); // the -Xmx limit in bytes
int[] tooBig = new int[Integer.MAX_VALUE]; // OutOfMemoryError: Requested array size exceeds VM limit
long[] huge = new long[Integer.MAX_VALUE - 8]; // OutOfMemoryError: Java heap space
Notice that the two failing lines give different messages, because the first array is above the JVM’s size limit and the second one fits the limit but not the heap. For a production crash, we start the app with a heap dump on error, find the cause, and decide after that whether a bigger heap is the right fix.
java -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/dumps -jar shop.jar
We go through what the error is, each detail message, the tools to find the cause, and the rare cases where catching the error makes sense.
1. What Is OutOfMemoryError?
The class OutOfMemoryError extends VirtualMachineError, which extends Error. It is unchecked, so no method declares it, and the compiler does not ask us to handle it. The JVM throws it in the thread that asked for the memory, which is not always the thread that used up the memory.
The JVM does not have one pool of memory. The Java heap holds objects, Metaspace holds class metadata, each thread has its own stack, and direct buffers and native libraries use memory outside the heap. Each area has its own limit and its own message, and the structure of the heap itself is explained in the Java memory model article.

Read the detail message before changing any option, because -Xmx fixes only the heap messages and does not help with Metaspace or thread limits. Each detail message points to one memory area and one first thing to check.
| Detail message | Memory area | First thing to check |
|---|---|---|
| Java heap space | Java heap | Data loaded at once, leaks, -Xmx |
| GC overhead limit exceeded | Java heap | Same as heap space; live data barely fits |
| Metaspace | Class metadata (native memory) | Class loader leaks, -XX:MaxMetaspaceSize |
| Requested array size exceeds VM limit | None, a JVM limit | An array length near Integer.MAX_VALUE |
| unable to create native thread | Operating system threads | Thread count, OS and container limits |
| Cannot reserve … bytes of direct buffer memory | Direct buffers (native memory) | Unreleased buffers, -XX:MaxDirectMemorySize |
2. Java Heap Space
The message Java heap space means that a new object did not fit into the heap after garbage collection. It does not always mean a leak; sometimes the heap is too small for the data the app holds at its peak. A small program that keeps adding 1 MB arrays to a list shows the message with a 64 MB heap.
java -Xmx64m Heap
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Heap.main(Heap.java:2)
The three usual causes need different fixes, so we look at them one at a time.
2.1. Loading Too Much Data at Once
An online shop imports a daily orders file. With 2,000 test rows, Files.readAllLines() works on every laptop. In production the file has 30 million rows, the list of strings needs several gigabytes, and the import job fails with Java heap space. Processing the file as a stream keeps only one line in memory at a time, and the article on reading a file line by line compares the APIs for it.
Path csv = Files.writeString(Files.createTempFile("orders", ".csv"), "id,cents\n1,250\n2,400\n3,150\n");
long total;
try (Stream<String> lines = Files.lines(csv)) {
total = lines.skip(1).mapToLong(line -> Long.parseLong(line.split(",")[1])).sum();
}
long sum = total; // 800
The same rule applies to database queries and HTTP responses. Read rows in pages or with a streaming result set, and never collect an unbounded result into one list.
2.2. Memory Leaks
A memory leak in Java is an object that the app no longer needs but still references, so the garbage collector cannot free it. The classic case is a static cache without a size limit.
final class PreviewService {
// Wrong: grows with every product and is never cleared
private static final Map<String, byte[]> PREVIEWS = new HashMap<>();
byte[] preview(String productId) {
return PREVIEWS.computeIfAbsent(productId, id -> new byte[200_000]);
}
}
Every product page adds a 200 KB preview, so a catalog of 50,000 products needs about 10 GB after a few days of traffic. A bounded cache keeps memory use constant. For small caches, a LinkedHashMap in access order with removeEldestEntry() is enough; for production caches with expiry, a caching library such as Caffeine is the better choice.
final class PreviewCache<K, V> extends LinkedHashMap<K, V> {
private final int maxEntries;
PreviewCache(int maxEntries) {
super(16, 0.75f, true); // access order
this.maxEntries = maxEntries;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxEntries;
}
}
PreviewCache<String, byte[]> previews = new PreviewCache<>(3);
for (String id : List.of("a", "b", "c", "d", "e")) {
previews.put(id, new byte[1024]);
}
int cached = previews.size(); // 3
Set<String> kept = previews.keySet(); // [c, d, e]
Other common leaks are listeners that are registered but never removed, ThreadLocal values in pooled threads that are never cleared, and sessions or request data stored in static fields.
2.3. A Heap That Is Too Small
When the heap dump shows only data the app needs, the heap is too small, and we raise the limit. The option -Xmx sets the maximum heap size and -Xms the initial size.
java -Xms1g -Xmx4g -jar shop.jar
Without -Xmx, the JVM uses 25% of the available memory as the maximum heap (-XX:MaxRAMPercentage=25). In a container, the JVM reads the container memory limit instead of the host memory, so a pod with a 2 GB limit gets a 512 MB heap by default. For containers, a percentage is easier to maintain than a fixed size, and the Docker memory and CPU limits decide what 100% means.
java -XX:MaxRAMPercentage=75.0 -jar shop.jar
We leave the remaining 25% for Metaspace, thread stacks, direct buffers and the JVM itself. Setting the heap close to the container limit makes the kernel kill the process instead of the JVM throwing an OutOfMemoryError.
3. GC Overhead Limit Exceeded
The message GC overhead limit exceeded means that the JVM spent more than 98% of its time in garbage collection while recovering less than 2% of the heap, over several collections in a row. The live data almost fills the heap, so the app makes almost no progress, and the JVM stops it early instead of letting it continue to run.
The Parallel collector applies the check by default (-XX:+UseGCOverheadLimit), and the default G1 collector gained it through JDK-8212084, which is in JDK 25.0.3 and later. With other collectors, or a different workload, the same situation ends with Java heap space, as it did when we ran a growing HashMap on JDK 25.0.4 with G1 and a 64 MB heap.
The fix is the same as for heap space, namely less live data, a leak fix or a bigger heap. Turning the check off with -XX:-UseGCOverheadLimit only delays the crash and leaves the app unresponsive in the meantime.
4. Metaspace
Metaspace holds the metadata of loaded classes, such as method code and constant pools. It lives in native memory, not in the Java heap, and by default it has no upper limit other than the machine’s memory. The message Metaspace appears only when a limit is set with -XX:MaxMetaspaceSize and the classes no longer fit.
A team redeploys a web app to the same Tomcat instance ten times a day. Each deployment creates a new class loader, and a JDBC driver that the app registered with DriverManager keeps a reference to the old one, so the old classes stay loaded. After enough redeploys, the server fails with Metaspace. We reproduced the effect with a small program that defines the same class in new class loaders and keeps them in a list.
java -XX:MaxMetaspaceSize=24m Meta
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:962)
The fix is to find what holds the old class loader, which in most apps is a static field, a thread started by the app, or a driver or logging framework, and to release it when the app stops. Raising MaxMetaspaceSize helps only when the app loads many classes legitimately, for example with many generated proxies.
Older articles and scripts still mention PermGen space and the options -XX:PermSize and -XX:MaxPermSize. PermGen was removed in Java 8, and current JDKs refuse to start with those options.
Unrecognized VM option 'MaxPermSize=512m'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
5. Requested Array Size Exceeds VM Limit
The message Requested array size exceeds VM limit means the requested array length is above the largest length the JVM supports, no matter how big the heap is. On HotSpot JDK 25, a byte array of Integer.MAX_VALUE – 2 elements works, while Integer.MAX_VALUE and Integer.MAX_VALUE – 1 fail with this message. An array that is below the limit but larger than the free heap fails with Java heap space instead.
The usual cause is a computed size that is wrong, such as a length read from a corrupt file header or an int overflow. For example, 100_000 * 100_000 overflows to 1,410,065,408, which is a valid length that needs more than 5 GB as an int array. Validate computed sizes and use Math.multiplyExact() where an overflow is possible.
int wrapped = 100_000 * 100_000; // 1410065408
int checked = Math.multiplyExact(100_000, 100_000); // ArithmeticException: integer overflow
6. Unable to Create Native Thread
Each platform thread needs an operating system thread and its own stack, which lives outside the heap. When the process reaches the OS limit for threads or memory, Thread.start() throws an OutOfMemoryError whose message names both possible reasons.
java.lang.OutOfMemoryError: unable to create native thread: possibly out of memory or process/resource limits reached
A chat backend that starts one platform thread per connected client works with 500 users and fails at 20,000. The fix is a bounded thread pool for CPU work, or virtual threads for tasks that mostly wait on I/O, since Java 21. Virtual threads do not need an OS thread each, so 10,000 tasks do not reach the OS thread limit.
int done;
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Integer>> futures = IntStream.range(0, 10_000).mapToObj(i -> executor.submit(() -> i)).toList();
done = futures.size();
}
int tasks = done; // 10000
When the thread count is legitimate, raise the OS limits, such as ulimit -u on Linux or the pids limit of the container, or lower the stack size per thread with -Xss.
7. Direct Buffer Memory
Direct buffers from ByteBuffer.allocateDirect() live in native memory. Network libraries such as Netty use them for I/O, so a leak there shows up as this message instead of Java heap space. The limit defaults to the maximum heap size and can be changed with -XX:MaxDirectMemorySize.
java -XX:MaxDirectMemorySize=16m Direct
Exception in thread "main" java.lang.OutOfMemoryError: Cannot reserve 4194304 bytes of direct buffer memory (allocated: 16777216, limit: 16777216)
at java.base/java.nio.Bits.reserveMemory(Bits.java:178)
at java.base/java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:108)
at java.base/java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:367)
The message shows the limit and how much was in use, which tells us whether to look for buffers that are never released or to raise the limit.
8. Finding the Cause With a Heap Dump
A heap dump is a file with every object in the heap and the references between them. It is the fastest way to tell a leak from a heap that is too small, so we enable it on every production JVM.
java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dumps/heap.hprof Heap
java.lang.OutOfMemoryError: Java heap space
Dumping heap to /tmp/dumps/heap.hprof ...
Heap dump file created [36327021 bytes in 0.056 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Heap.main(Heap.java:2)
For a running process that grows but has not failed yet, the jcmd tool from the JDK takes a dump or a quick class histogram without restarting the app.
jcmd <pid> GC.class_histogram | head -15
jcmd <pid> GC.heap_dump /tmp/dumps/live.hprof
We open the .hprof file in a heap analyzer and look at the dominator tree, which shows the objects that keep the most memory alive.
- Eclipse Memory Analyzer (MAT) is free and its leak suspects report points to the largest retained object graphs.
- VisualVM is a free GUI that monitors heap, threads and classes of a live JVM and opens heap dumps. It has been a separate download since JDK 9.
- JDK Mission Control reads JDK Flight Recorder files, and its Old Object Sample event shows objects that stay alive for a long time, with the stack trace that allocated them.
- Commercial profilers, such as JProfiler and YourKit, offer the same views plus allocation recording.
In containers, add -XX:+ExitOnOutOfMemoryError as well. A JVM that survived an OutOfMemoryError can be in a broken state, and exiting lets the orchestrator restart a clean instance.
9. Should We Catch OutOfMemoryError?
In most code, no. The error can occur in any thread at any allocation, including inside library code that was in the middle of changing shared state, so the app may not be consistent after it. Logging the error in a top-level handler and letting the process exit is the safe default.
One narrow case is acceptable. When code tries one large, optional allocation, such as a big buffer that speeds up an export, it can catch the error around that single allocation and fall back to a smaller strategy.
static Optional<long[]> tryAllocate(int size) {
try {
return Optional.of(new long[size]);
} catch (OutOfMemoryError e) {
return Optional.empty(); // fall back to a smaller buffer
}
}
boolean bigBuffer = tryAllocate(Integer.MAX_VALUE).isPresent(); // false
int smallBuffer = tryAllocate(1_000).map(b -> b.length).orElse(0); // 1000
Errors and exceptions sit in different branches of Throwable, and checked vs unchecked exceptions explains why the compiler never forces us to handle an Error.
10. OutOfMemoryError FAQs
Production incidents and container restarts lead to most of the questions about this error.
10.1. Why was my container killed without an OutOfMemoryError?
Because the process used more memory than the container limit, and the Linux kernel killed it; Kubernetes reports this as OOMKilled with exit code 137. The JVM throws OutOfMemoryError only when one of its own limits is hit first. Keep the heap well below the container limit, for example with -XX:MaxRAMPercentage=75.0, so the JVM limits trigger before the kernel’s.
10.2. What is the default maximum heap size?
The JVM uses 25% of the physical memory, or of the container memory limit, as the default maximum heap. The command java -XX:+PrintFlagsFinal -version | grep MaxHeapSize prints the value, and Runtime.getRuntime().maxMemory() returns it inside the app.
10.3. Does calling System.gc() prevent OutOfMemoryError?
No. Before the JVM throws OutOfMemoryError for the heap, it has already run a full garbage collection. If memory is still not enough after that, an extra System.gc() call cannot free anything.
10.4. Is OutOfMemoryError checked or unchecked?
Unchecked. It extends Error through VirtualMachineError, so methods do not declare it and the compiler never asks us to catch it.
10.5. What is the difference between OutOfMemoryError and StackOverflowError?
An OutOfMemoryError means a memory area such as the heap is full. A StackOverflowError means one thread’s stack is full, nearly always because of deep or endless recursion, and -Xss controls its size.
11. Conclusion
An OutOfMemoryError always names the memory area that ran out. Java heap space and GC overhead limit exceeded point to the heap, where we stream large data, bound caches and size the heap for the real workload. Metaspace points to class loader leaks, and thread and direct buffer messages point to native memory that -Xmx does not control.
Enable -XX:+HeapDumpOnOutOfMemoryError on every production JVM, analyze the dump before changing limits, and size the heap as a percentage of the container memory. Catching the error is reserved for a single optional allocation with a fallback.
12. References
- Oracle, Troubleshoot Memory Leaks (JDK 25)
- OutOfMemoryError Javadoc (Java 25)
- java command, JVM options (Java 25)
- JDK-8212084, G1 support for UseGCOverheadLimit
Happy Learning !!