⚙️ JVM Internals & Memory · Advanced

OutOfMemoryError types in Java

Java heap space, Metaspace, GC overhead limit, unable to create native thread.

🧩 The mysteryYour server has 32 GB of RAM and a 4 GB heap that's half empty, yet it crashes with OutOfMemoryError: Metaspace. How?

Read the message first

OutOfMemoryError tells you which memory ran out. Java heap space: live objects filled the heap. Metaspace: too many classes. GC overhead limit exceeded: the JVM spends almost all its time collecting and frees almost nothing. unable to create native thread: the OS refused another thread.

Classes die with their loader

A class can be unloaded only when its whole defining class loader is unreachable. Every hot redeploy creates a new loader with fresh copies of every class. If anything still references the old loader (a static, a ThreadLocal, a JDBC driver registry), all its classes stay in metaspace.

🔮 Predict it

Just add heap?

Production throws "OutOfMemoryError: unable to create native thread". Will raising -Xmx from 4g to 8g fix it?

  1. Yes, threads live in the heap
  2. No: threads need OS resources and native memory
Show the answer

No. Platform threads and their stacks use native memory and OS limits, outside the heap. A bigger heap can even leave less native memory. Cap threads with a pool, or use virtual threads.

🔮 Predict it

What kind of Throwable?

What does this print?

Throwable t = new OutOfMemoryError("Metaspace");
System.out.println(t instanceof Exception);
System.out.println(t instanceof Error);
System.out.println(t.getMessage());
  1. false true Metaspace
  2. true false Metaspace
  3. true true null
Show the answer

OutOfMemoryError extends **Error**, not Exception, so it's unchecked: no method has to declare it. The message string names the exhausted area.

⚠️ The trap

Catch it and carry on?

Catching OutOfMemoryError to keep going is a trap: it can hit any thread at any allocation, leaving half-updated state behind. It signals a serious JVM problem. Find the cause; don't hide it.

try {
    process(batch);
} catch (OutOfMemoryError e) {
    // "just retry"? state may be corrupt
}
💼 In the real world

Capture evidence automatically

Run production with **-XX:+HeapDumpOnOutOfMemoryError** (and -XX:HeapDumpPath). The JVM writes an .hprof heap dump at the moment of the crash, so you can see which objects filled the heap and what kept them alive, instead of guessing after a restart.

Key takeaways

  1. Read the message: it names the exhausted area
  2. Metaspace OOM often means a class loader leak
  3. Native thread OOM is fixed with fewer threads, not more heap
  4. -XX:+HeapDumpOnOutOfMemoryError captures evidence
🤯 Did you know?

With the Parallel collector, "GC overhead limit exceeded" is thrown when more than 98% of the time goes to GC while less than 2% of the heap is recovered.

Practice questions

After the 20th hot redeploy on an app server, the app dies with OutOfMemoryError: Metaspace. Most likely cause?

  1. Too many threads were started
  2. Strings are interned in metaspace
  3. The heap (-Xmx) is too small
  4. Old class loaders are still referenced, so their classes are never unloaded
Check your answer

Old class loaders are still referenced, so their classes are never unloaded. Each redeploy creates a new class loader with fresh copies of every class. If anything (a static, a ThreadLocal, a JDBC driver registry) still references the old loader, all its classes stay in metaspace.

Production throws "OutOfMemoryError: unable to create native thread". A teammate suggests raising -Xmx from 4g to 8g. Good idea?

  1. No: thread stacks use native memory and OS limits; cap threads with a pool or use virtual threads
  2. No: you should lower -Xss to 0
  3. Yes, threads are allocated in the heap
  4. Yes, a bigger heap always fixes OOM
Check your answer

No: thread stacks use native memory and OS limits; cap threads with a pool or use virtual threads. Platform threads need OS resources and native stack memory outside the heap. A bigger heap can even leave less native memory, so the real fix is fewer platform threads.

Memory is half of the JVM story; speed is the other half. Next: why the same method gets faster the more you call it. Meet the JIT.