java.lang.OutOfMemoryError: Java heap space

Quick answer

The heap filled up and GC couldn't free enough to satisfy an allocation. Don't raise -Xmx yet — first find out which problem you have:

  • Post-GC memory returns to a flat baseline → heap is just too small; raise -Xmx.
  • Post-GC baseline creeps upward forever → memory leak; a bigger heap only delays the crash.
  • One giant allocation (a huge file/query) → stream it instead of loading it whole.
  • In a container? Use -XX:MaxRAMPercentage, not a hardcoded -Xmx.

The exact error string

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.Arrays.copyOf(Arrays.java:3512)
        at java.base/java.lang.AbstractStringBuilder.ensureCapacity(AbstractStringBuilder.java:239)
        at com.example.ReportBuilder.build(ReportBuilder.java:88)
        at com.example.Main.main(Main.java:24)

An important thing to understand up front: the stack trace shows where memory ran out, not what caused it. The frame at the top is simply the unlucky allocation that happened to be the last straw — often something innocuous like growing a StringBuilder or copying an array. The objects actually filling the heap were usually allocated somewhere else entirely, possibly minutes earlier. Chasing the line in the stack trace is the single most common wasted afternoon with this error.

Step 1: leak, or just too small?

Everything downstream depends on this one question, and it has a precise answer. Watch what happens to heap usage after each full garbage collection — the low points, not the peaks:

Healthy — heap just too small flat post-GC baseline stays level → raise -Xmx (Fix 1) Leak — retention grows -Xmx ceiling post-GC baseline climbs → OOM → find the leak (Fix 2) Compare the LOW points, not the peaks -Xlog:gc* — or a profiler — shows the baseline flat = size problem · rising = retention problem

Peaks always look alarming in both cases. Only the post-GC baseline distinguishes a heap that's too small from one that's leaking.

# log every GC with heap before/after — the cheapest way to see the baseline
java -Xlog:gc* -jar app.jar

# JDK 8 equivalent
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar app.jar

# live view of a running process
jcmd <pid> GC.heap_info
jstat -gcutil <pid> 5s

Fix 1: the heap is genuinely too small

If the baseline is flat and the application simply needs more room at peak load, raising the maximum heap is the right and complete fix:

java -Xms1g -Xmx2g -jar app.jar

Setting -Xms (initial) equal to -Xmx (maximum) is common for servers: it avoids the cost of growing the heap incrementally and makes memory behaviour predictable from the first request. The trade-off is that the process reserves that much up front.

Be aware that a bigger heap is not free. Full-GC pauses scale with heap size, so a very large heap can turn a memory problem into a latency problem. If you're pushing past a few gigabytes, it's usually worth switching collectors (G1 is the default on modern JDKs; ZGC or Shenandoah for low-pause requirements) rather than just growing the number.

Fix 2: it's a leak — find what's retaining memory

If the baseline climbs, no -Xmx value will save you; it just moves the crash later. Capture a heap dump and find what's holding the objects alive:

# write a dump automatically when it fails — set this in production ahead of time
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp -jar app.jar

# or take one on demand from a running process (live = after a full GC)
jmap -dump:live,format=b,file=heap.hprof <pid>

Open the .hprof in Eclipse MAT (the standard tool) or VisualVM and run the Leak Suspects report. The critical view is the dominator tree: it doesn't just show which objects are biggest, it shows the reference chain keeping them alive — which is the actual answer to "why isn't this being collected?"

The recurring culprits, roughly in order of how often they turn up:

PatternWhy it retainsFix
Unbounded cache (a static Map that only grows)Nothing ever evicts entriesUse Caffeine/Guava with a size or TTL bound
Listeners / callbacks registered, never removedThe registry holds a strong reference foreverDeregister in a close()/lifecycle hook
ThreadLocal not cleared on pooled threadsThe thread outlives the request, so the value does tooremove() in a finally block
Long-lived static collectionsClass-level roots are never collectedScope to a bean/instance, or bound the size
Unclosed streams/connectionsNative and buffer memory pinnedtry-with-resources

Fix 3: one huge allocation — stream instead of loading

Sometimes there's no leak and the heap isn't unreasonably small: a single operation just tries to materialise something enormous. Reading a large file, or a query with no LIMIT, into a list is the usual shape:

// ❌ entire file in memory at once
List<String> lines = Files.readAllLines(Path.of("huge.log"));

// ✅ stream it — one line in memory at a time
try (Stream<String> lines = Files.lines(Path.of("huge.log"))) {
    lines.filter(l -> l.contains("ERROR")).forEach(this::handle);
}

// ❌ unbounded result set — the whole table lands in a List in memory
List<Row> all = jdbc.query("SELECT * FROM events", rowMapper);

// ✅ page with LIMIT/OFFSET (or a keyset), or stream with a real server-side cursor
jdbc.setFetchSize(1000);

setFetchSize is a hint, not a guarantee — most drivers happily ignore it and buffer the full result set anyway unless additional conditions are met. On PostgreSQL it only takes effect with auto-commit off (connection.setAutoCommit(false)); on MySQL the driver requires useCursorFetch=true in the connection URL, or a MySQL-specific streaming mode via Statement.setFetchSize(Integer.MIN_VALUE), which has its own restrictions (e.g. you must read the whole result set before issuing another query on that connection). Check your driver's own documentation before assuming setFetchSize alone keeps a large query off the heap — the safer bet for genuinely huge result sets is explicit paging (LIMIT/OFFSET or a keyset) at the query level.

The same trap applies to parsing a large JSON document with a tree-based parser: the whole object graph lands on the heap at once. Streaming parsers (Jackson's JsonParser, or Gson's JsonReader) process it incrementally instead. If you're working out the shape of a large payload before deciding how to parse it, our JSON Formatter is a quick way to inspect its structure without writing throwaway code — and the equivalent decode problems in Go follow the same "decode into a typed target, don't hold it all" principle.

Fix 4: sizing the heap inside a container

This deserves its own treatment, because the failure mode is different and confusing. In Docker or Kubernetes there are two ceilings: the JVM's -Xmx, and the container's memory limit enforced by the kernel. Which one you hit determines what you see:

What happensWhich ceilingSymptom
JVM heap exhausted-XmxOutOfMemoryError: Java heap space (this page) — the JVM throws, stack trace available
Process exceeds container limitcgroup limitKilled with exit code 137 (OOMKilled) — no Java exception at all

The second case surprises people because there's no Java error to read — the process simply vanishes. It happens when -Xmx is set close to the container limit, leaving no headroom for everything that lives outside the heap: thread stacks, metaspace, the JIT code cache, GC structures, and native/direct buffers. Those can easily add several hundred megabytes.

# ❌ container limit 512m, heap allowed 512m — no room for non-heap memory
docker run -m 512m myapp java -Xmx512m -jar app.jar

# ✅ let the JVM size itself from the container limit, with headroom
docker run -m 512m myapp java -XX:MaxRAMPercentage=75.0 -jar app.jar

Modern JDKs (10+) are container-aware by default — UseContainerSupport is on, so the JVM reads the cgroup limit rather than the host's total RAM. -XX:MaxRAMPercentage is therefore the better flag in containers: it scales automatically when you change the limit, instead of silently becoming wrong the next time someone edits the deployment.

Related OutOfMemoryError variants

The message after OutOfMemoryError: tells you which region ran out, and they have different fixes:

MessageMeaning
Java heap spaceThe object heap is full — this page
GC overhead limit exceeded>98% of time in GC recovering <2% — effectively the same condition, caught earlier. Mostly seen with Parallel GC; G1, the default collector since JDK 9, typically throws plain Java heap space instead
MetaspaceClass metadata, not objects — usually classloader leaks (frequent redeploys)
Requested array size exceeds VM limitA single array larger than the JVM allows — almost always a bug in a size calculation
unable to create new native threadOS thread limit, not heap — usually an unbounded thread pool

Debugging checklist

Frequently Asked Questions

What does java.lang.OutOfMemoryError: Java heap space mean?

The JVM tried to allocate an object, the heap had no room, and the garbage collector could not free enough space to satisfy the request. It is not a crash in your code so much as a resource exhaustion: either the application genuinely needs more heap than it was given, or it is holding on to objects it should have released — a memory leak.

Should I just increase -Xmx?

Only after you know which problem you have. If memory usage rises to a stable plateau and the workload genuinely needs more room, raising -Xmx is the correct fix. If usage climbs steadily until it hits whatever ceiling you set, that is a leak — a bigger heap only postpones the same crash and makes the eventual garbage-collection pauses worse. Check the trend before changing the flag.

How do I tell a memory leak from an undersized heap?

Watch heap usage after full garbage collections over time. If the post-GC baseline stays flat, the heap is merely too small for peak load. If the post-GC baseline creeps steadily upward — never returning to its earlier level — objects are being retained and you have a leak. Enable GC logging with -Xlog:gc*, or watch live in a profiler, and compare the low points rather than the peaks.

How do I capture and read a heap dump?

Add -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp so the JVM writes a dump automatically when it fails, or take one on demand with jmap -dump:live,format=b,file=heap.hprof <pid>. Open the .hprof file in Eclipse MAT or VisualVM and run a leak-suspects report — it ranks which objects dominate the heap and, more usefully, shows the reference chain keeping them alive.

Why does my Java container get killed instead of throwing OutOfMemoryError?

Those are two different failures. OutOfMemoryError means the JVM hit its own heap ceiling and threw. Being killed with exit code 137 means the kernel's OOM killer terminated the whole process for exceeding the container's memory limit — no Java exception is thrown at all. That usually means -Xmx was set too close to the container limit, leaving no headroom for thread stacks, metaspace and native buffers, which live outside the heap.

What are the most common causes of a Java heap leak?

An unbounded collection used as a cache with no eviction is the classic — a static Map that only ever grows. Others include listeners or callbacks registered but never removed, ThreadLocal values not cleared on pooled threads, unclosed resources holding buffers, and objects held alive by long-lived static references. A heap dump's dominator tree points at whichever applies, because it shows what is actually retaining the memory.

How is this different from 'GC overhead limit exceeded'?

They are two symptoms of the same underlying condition. ‘Java heap space’ means an allocation outright failed. ‘GC overhead limit exceeded’ means the JVM is spending over 98% of its time collecting and recovering less than 2% of the heap — it gave up before allocation failed outright. Both indicate the heap is effectively exhausted, and both are diagnosed the same way. In practice you're more likely to see it on Parallel GC (where the check originates); G1, the default collector since JDK 9, typically throws plain ‘Java heap space’ instead of this variant.

More Java & memory errors

Browse the full reference for Java, Go, and Rust errors — exact message, cause, and fix.

All Error References Docker: exit code 137 (OOMKilled) Node: JS heap out of memory
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.