Definition
OutOfMemoryError (OOM) is thrown when the JVM cannot allocate memory and the garbage collector cannot free enough to satisfy the request. The first diagnostic step is reading the error's message to learn which region ran out, because each points to a different cause: "Java heap space" means the heap is exhausted, "Metaspace" means class metadata exhausted, "unable to create new native thread" means the OS could not create another thread, and "GC overhead limit exceeded" means GC is running constantly while freeing almost nothing. You then capture a heap dump and analyze it in Eclipse MAT for large object retention or a leak.
Understanding the Concepts
OutOfMemoryError: Java heap space
This is the most common variant. It means the heap is full of live objects. The cause is either a genuine need for more memory (undersized -Xmx), a memory leak where objects stay reachable forever, or a spike like loading a huge result set into memory at once. You diagnose it with a heap dump: the Dominator Tree in Eclipse MAT shows which objects retain the most heap, and the path to GC roots shows what holds them.
package com.bank.report;
import java.util.ArrayList;
import java.util.List;
public class StatementGenerator {
// BAD: loads all transactions into memory at once
public void generateYearlyStatement(String accountId) {
List<Transaction> allTxns = repo.findAllByAccountId(accountId);
// For an active account, this could be millions of rows → OOM!
}
// GOOD: stream and process in pages
public void generateYearlyStatementPaged(String accountId) {
int page = 0;
List<Transaction> batch;
do {
batch = repo.findByAccountId(accountId, page, 1000);
processBatch(batch);
page++;
} while (!batch.isEmpty());
}
}
OutOfMemoryError: Metaspace
This means class metadata has filled Metaspace. Since Metaspace holds loaded classes, this usually indicates a class-loading leak: frameworks that generate proxy classes endlessly, repeated redeployments in an app server that leak old class loaders, or dynamic scripting that keeps defining classes.
# Monitor loaded class count over time
jcmd <pid> VM.classloader_stats
# If loaded classes climb indefinitely, you have a class-loader leak
# Bound Metaspace so it fails fast instead of consuming all native memory
java -XX:MaxMetaspaceSize=256m -jar bank-api.jar
You diagnose it by watching loaded-class count climbing over time and by capturing a dump to see which class loaders are retained.
OutOfMemoryError: unable to create new native thread
This is not about the heap at all. It means the operating system refused to create another thread, usually because the process hit a thread-count or memory limit. Each thread needs a native stack (default around 1 MB, set by -Xss), so thousands of threads consume real memory outside the heap.
package com.bank.service;
public class BadBatchProcessor {
// BAD: creates a new thread per account – thousands of threads
public void processAllAccounts(List<String> accountIds) {
for (String id : accountIds) {
new Thread(() -> processAccount(id)).start(); // unbounded!
}
}
// GOOD: use a bounded thread pool
public void processAllAccountsCorrectly(List<String> accountIds) {
ExecutorService pool = Executors.newFixedThreadPool(16);
for (String id : accountIds) {
pool.submit(() -> processAccount(id));
}
pool.shutdown();
}
}
The cause is often a thread leak – creating threads without a bounded pool – and the fix is to use a bounded thread pool and check OS ulimits.
OutOfMemoryError: GC overhead limit exceeded
This means the JVM spent over 98 percent of time in GC while recovering less than 2 percent of the heap. It is an early warning of a heap-space problem, treated the same way with a heap dump.
Diagnostic workflow
Step 1: Read the OOM message → identifies which memory region Step 2: Capture heap dump (auto or manual) Step 3: Open in Eclipse MAT → Dominator Tree + Leak Suspects Step 4: Trace path to GC roots → find the retaining reference Step 5: Correlate with GC logs → leak (drift up) vs spike (sudden) Step 6: Fix in code, verify with load test
What should a senior engineer know?
- Always run production with
-XX:+HeapDumpOnOutOfMemoryErrorand-XX:HeapDumpPathso the dump is captured automatically. - Correlate the OOM with GC logs to see whether the heap baseline was drifting up (leak) or spiked suddenly (bad request).
- Never just raise
-Xmxto fix a leak. That only delays the crash. Find and fix the root cause. - In Kubernetes, set the JVM heap smaller than the container memory limit to leave room for Metaspace, thread stacks, and native memory. A common formula:
-Xmx= container limit minus 500 MB. - In banking at National Bank of India, Rajesh always sets up OOM heap dumps and GC log alerts on every production service, so the team never has to guess when a memory issue occurs.
Key Takeaway
When I hit an OutOfMemoryError, the very first thing I do is read the error message, because it tells me which region of memory ran out, and each region points to a completely different cause and fix. If it says "Java heap space," the heap is full of live objects, which means either a genuine need for a bigger heap, a memory leak, or a request that tried to load far too much into memory at once. If it says "Metaspace," class metadata is exhausted, which almost always means a class-loader leak from things like proxy generation or repeated redeployments. If it says "unable to create new native thread," it is not a heap problem at all; the operating system refused another thread, usually because of a thread leak or an OS limit, since every thread needs a native stack outside the heap. And "GC overhead limit exceeded" is an early warning of a heap problem, where GC is running almost constantly while freeing almost nothing.
Once I know the region, I take a heap dump and analyze it. In production I always run with heap-dump-on-OutOfMemoryError and a dump path set, so the JVM captures a full snapshot at the exact moment of failure instead of losing the evidence. I open that dump in Eclipse Memory Analyzer, where the Dominator Tree shows which objects retain the most memory and the path to GC roots shows exactly what is holding them alive. I also correlate with the GC logs to tell whether the heap baseline was drifting up slowly, which signals a leak, or spiked suddenly, which signals a bad request.
In my banking work at National Bank of India, I have diagnosed all three of these. Our reporting service crashed with "Java heap space" because a report tried to load an entire year of transactions into one list, and the heap dump made it obvious with a single giant list dominating memory, so we fixed it by streaming and paginating instead. On the same app server, after many redeployments, we hit "Metaspace" from leaked class loaders, which we bounded with MaxMetaspaceSize so it failed fast and alerted. And a batch job that spawned a thread per account instead of using a pool failed with "unable to create new native thread," which we fixed with a bounded thread pool. The habit that saves me every time is to read the message first and never just raise the heap size, because that only delays a leak instead of fixing it.
Remember one sentence: How to diagnose OutOfMemoryError: check which area ran out (Heap space, Metaspace, unable to create native thread), take heap dump, analyze for large object retention or memory leaks is fundamental knowledge for Java interviews and production banking work.
---