Java production systems usually provide robustness, even when using substantial, long-term throughputs. But when a failure occurs, we need to look at the memory. Let’s assume that a service that processes thousands of requests per minute ends with a StackOverflowError under one load pattern. But the same service with a different execution pattern fails with OutOfMemoryError: Unable to create new native thread. On the surface, they seem unrelated, where one implies overheating the stack and the other is evidence of exhaustion of thread allocation.
In both cases, the incidents and errors are different. The two errors accordingly, represent opposite failure modes at the same thread memory boundary. In this blog, we examine:
- StackOverflowError
- OutOfMemoryError: Unable to create new native thread
We also look into how they occur, how they differ, and how to diagnose and fix them in production settings.
JVM Memory Model
Before comparing StackOverflowError and OutOfMemoryError: Unable to Create New Native Threads, it is important to know where thread execution actually resides inside the JVM memory model.

Fig: Java Memory
Some developers think of ‘heap’ and ‘memory’ as being synonymous, but not all memory in a Java application is’ heap’. Others think of memory as being divided into ‘heap’ and ‘stack’. However, if we look closer at the JVM memory model, we can visualize it as shown in the diagram above.
Memory is in fact divided into heap memory and native memory. The stack is a part of native memory, which is held in the ‘Threads’ area. Let’s define these terms.
Heap Memory
This is where Java objects are allocated and garbage is collected. Most memory tuning discussions focus on these: GC pauses, heap sizing, and object retention.
Native Memory
This is memory allocated outside the Java heap at the OS level, encompassing thread stacks, direct buffers, JNI allocations, and JVM internal structures.
Stack Memory
Thread stacks reside within native (non-heap) memory and are allocated per Java thread. Each stack holds method execution frames containing local variables, parameters, intermediate operands, and return data. As methods are invoked and completed, frames are pushed to and popped from the stack, enabling structured execution flow within each thread.
But if you want a detailed insight into these JVM Memory regions, then you should take a look at this video where yCrash Architect has explained it in 10 minutes.
Note: Also if you wish to understand the structural differences between heap memory and native memory, and how thread stacks are allocated outside the heap, you can explore this detailed breakdown in Heap vs Native Memory.
What is StackOverflowError?
Basically, StackOverflowError happens when Java threads use up their stack memory. Each thread of a JVM receives a fixed stack space specified by the -Xss parameter for stack usage. This stack contains method execution frames, which include local variables, parameters, operand data, and return information. When we invoke a method, one new frame is pushed, and after the method completes, the frame is popped. Problems occur when the stack depth exceeds its limit. This is often because of uncontrolled or deeply nested method calls, most frequently infinite recursion or badly bounded recursion, but excessive call chaining in highly complex execution paths can contribute as well. Then, when the JVM can no longer allocate space for an additional frame, it throws a StackOverflowError and terminates the offending thread.
Real-World Illustration: Infinite Recursion Causing StackOverflowError
To understand how stack exhaustion occurs at the execution level, consider the following simplified program:
public class SOFDemo { public static void a() {// Buggy line. It will cause method a() to be // called infinite number of times. a(); } public static void main(String args[]) { a(); }}
When the program starts, the JVM creates the main thread, allocating stack space for it (controlled by -Xss).
The program executes as follows:
- main() is invoked → A stack frame is created.
- main() calls a() → A new frame is pushed.
- Inside a(), the method calls itself again.
- Each recursive call pushes another frame onto the same thread stack.
- No termination condition exists → recursion never stops.
You’ll notice that each frame consumes memory for local variables, parameters, operand stacks and return metadata. As recursion continues, frames accumulate until the thread exhausts its allocated stack space.

Fig: Progressive stack frame accumulation leading to StackOverflowError
When the JVM attempts to push one more frame but finds no remaining stack capacity, it throws:
Exception in thread "main" java.lang.StackOverflowError
At this point, the threads cannot continue the execution, the JVM terminates the thread, and no additional frame can be allocated.
If you would like to see what happens behind the scenes when such a program is executed, and to understand how thread dumps expose these recursive call patterns, and how the issue can be systematically diagnosed and resolved, you can refer to this detailed StackOverflowError analysis.
What is OutOfMemoryError: Unable to Create New Native Threads
On a high level, OutOfMemoryError: Unable to Create New Native Threads occurs when the JVM fails to create more threads at the OS level. For stack allocation, every Java thread requires native memory. When the JVM spins up a new thread, it requests memory and thread resources from the OS. When either the system thread capacity or the native thread limit is exhausted, the JVM fails the request and throws this error.

Fig: ‘java.lang.OutOfMemoryError: Unable to create new native threads’ (Image source: https://blog.heaphero.io/types-of-outofmemoryerror/)
Unlike heap-related OutOfMemoryErrors, which are in response to object allocation pressure, this failure is due to a limit on thread creation.
Sample Program: Simulating Native Thread Exhaustion
Here’s a sample program to simulate ‘java.lang.OutOfMemoryError: Unable to create new native threads’ when executed:
public class ThreadLeakDemo { public static void start() { while (true) { try { Thread.sleep(100); } catch (Exception e) { } new ForeverThread().start(); } }}public class ForeverThread extends Thread { @Override public void run() { try { // sleep for 2 hours Thread.sleep(120 * 60 * 1000); } catch (Exception e) {} }}
The above program contains a ThreadLeakDemo#start() method, within which an infinite while (true) loop continuously launches ForeverThread every 100 milliseconds. Each ForeverThread, in turn, is programmed to sleep for two hours before terminating.
This behavior results in rapid, uncontrolled thread accumulation. Since threads remain active for extended durations, the JVM keeps allocating native memory for their stack space. Over time, the application becomes flooded with ForeverThread instances, steadily consuming native memory.
Once the cumulative thread stack allocation breaches the system’s native memory or OS thread limit, the JVM fails to create additional threads and throws:
java.lang.OutOfMemoryError: Unable to create new native threads
To understand how such uncontrolled thread growth manifests in thread dumps, and how native memory pressure is diagnosed and resolved in real-world scenarios, you can explore this detailed analysis in OutOfMemoryError: Unable to Create New Native Threads.
Thread Stack Analysis: Where They Intersect
When you first look at the StackOverflowError and OutOfMemoryError: Unable to Create New Native Threads, they both appear to be unrelated failures. One terminates an executing thread, while the other prevents new threads from being created.
But that is not the case. Both the errors originate from the same operational boundary: thread stack memory.
Every Java thread is allocated a dedicated stack. This stack consumes native memory and is governed by the configured stack size (-Xss). If there is a failure, then it occurs when this stack resource is either over-consumed by execution depth or over-provisioned across thread volume.
To be more precise:
- One failure is driven by how deep a thread executes.
- The other is driven by how many threads the JVM attempts to host.
The distinction becomes clearer when analyzed side by side.
Difference Between StackOverflowError and OOM: Unable to Create New Native Threads
Here’s a comparison table that purely focuses on the differences between the two errors:
| Aspect | StackOverflowError | OutOfMemoryError: Unable to Create New Native Threads |
| Definition | Occurs when a thread exhausts its stack memory due to excessive execution depth | Occurs when the JVM cannot create new threads due to native memory or OS thread limits |
| Failure Scope | Impacts an individual thread | Impacts the JVM’s ability to create threads system-wide |
| Primary Trigger | Deep or infinite recursion, excessive method calls | Uncontrolled thread creation, thread leaks, high concurrency |
| Memory Area Involved | Thread Stack (per thread) | Native Memory (aggregate thread stacks) |
| Growth Pattern | Depth-driven (vertical stack growth) | Volume-driven (horizontal thread growth) |
| Heap Memory Impact | No direct impact | No direct impact |
| JVM Stability | JVM may continue running, but the current thread terminates | JVM may stall, reject requests, or crash |
| Observed Symptoms | Repeating stack frames in stack trace | Very high thread count, thread creation failures |
| Common Production Causes | Recursive retries, deep frameworks, cyclic calls | Unbounded executors, blocking I/O, traffic spikes |
| Tuning Lever | Increase -Xss, refactor recursion | Limit thread pools, increase native memory, decrease -Xss |
| Reproducibility | Easily reproducible via code path | Load and environment dependent |
Diagnostic & Remediation Comparison
Although diagnostic steps were explored within each error’s section, the troubleshooting approach differs fundamentally based on failure behavior.
| If You Observe | Investigate For |
| Deep, repeating stack traces | Recursive execution → StackOverflowError |
| Extremely high thread counts | Thread proliferation → Native Thread OOM |
| Stable heap, failing requests | Thread stack / native memory pressure |
| Failures during method execution | Stack depth exhaustion |
| Failures during thread creation | Native capacity exhaustion |
In practice, StackOverflowError investigations remain code-path centric, relying on stack trace analysis, whereas Native Thread OOM diagnostics extend into JVM configuration, executor governance, and OS-level thread limits.
Reproduction via Chaos Engineering
Failures such as StackOverflowError and OutOfMemoryError: Unable to Create New Native Threads often do not present themselves on routine tests because they depend on runtime execution behavior and load conditions. That is why Chaos Engineering is adopted, as it provides a controlled approach to intentionally simulating these failure modes before they impact production systems.
For StackOverflowError, engineers can introduce controlled recursion faults (e.g., removal of termination conditions, amplified retry loops) and also load execute the service. Stack frames accumulate and the JVM ends up violating the defined stack limit and the error is initiated.
To explore how such recursion faults are systematically injected, observed, and analyzed, refer to this chaos engineering walkthrough on StackOverflowError.
Similarly, for OutOfMemoryError: Unable to Create New Native Threads, typically reproduces by simulating uncontrollable thread growth, using unbounded executors or high-concurrency load patterns; it uses up native memory and OS thread resources. A detailed troubleshooting and reproduction guide can be found here.
JVM Tuning Trade-off: Stack Size vs Thread Count
In addition, the size of the thread stack significantly affects the stack depth capacity as well as the total thread scalability within the JVM. Stack memory is allocated to each thread depending on the -Xss value that has been configured. This allocation is from native memory, so there is a direct trade-off between how deep threads can run and how many threads the JVM can sustain.
Here’s a simple mathematical representation of the trade-off:
Available Native Memory / Stack Size = Max Threads
Note: We should only consider tuning the application if we’re sure the problem is not caused by a coding issue. Fix the code first, then consider tuning if we need to.
Real-World Production Case Patterns
Even though StackOverflowError and OutOfMemoryError: Unable to Create New Native Threads differ in failure mechanics, they tend to surface in predictable production patterns.
StackOverflowError finds its way into the following:
- Recursive retry or fallback workflows
- Deep serialization/deserialization paths
- Framework proxy or interceptor chains
- Rule engines and expression evaluators
These failures are execution-path specific and often triggered by edge-case inputs or downstream failures.
And Native Thread OOM typically emerges in:
- High-concurrency microservices
- Blocking I/O architectures
- Unbounded executor configurations
- Traffic spikes combined with latency
Here, the failure emerges from thread volume growth rather than execution depth.
Recognizing these environmental patterns accelerates root cause isolation during incidents.
Prevention Checklist
They say prevention is better than cure. So here’s a quick checklist to prevent your production environment from these errors:
| StackOverflowError (Depth-oriented safeguards) | OOME: Unable to Create New Native Threads (Capacity-oriented safeguards) |
| 1. Enforce recursion termination conditions 2. Cap retry attempts and fallback loops 3. Review deep call hierarchies in frameworks 4. Load test complex execution paths | 1. Use bounded thread pools 2. Avoid thread-per-request models 3. Align thread counts with container memory 4. Monitor thread growth trends |
If you still face the issue, then no worries. We got you covered. The next section discusses the tools that can help you overcome the production issues caused by these errors.
Tooling & Observability
Diagnosing thread stack failures requires visibility beyond heap metrics. The following tools help engineers analyze execution depth, thread volume, and native memory pressure across both error scenarios.
| Tool | Helps Diagnose | What You Can Observe |
| fastThread | StackOverflowError | Recursive call patterns, deep stack traces, problematic execution paths |
| HeapHero | OutOfMemoryError: Unable to Create New Native Threads | Native memory consumption, thread allocation impact |
| yCrash | Both errors | Automated RCA correlating thread dumps, logs, and system metrics |
| Thread Dump (jstack / kill -3) | Both errors | Stack depth, repeating call frames, total thread count |
| JVisualVM / JMC | Native Thread OOM | Live thread count, thread states, resource utilization |
| OS Tools (top, ps, htop) | Native Thread OOM | System-level thread volume, CPU saturation |
| ulimit / OS configs | Native Thread OOM | Max thread/process limits enforced by OS |
| Kubernetes / Container Metrics | Native Thread OOM | Memory limits, pod restarts, thread scaling under load |
| Application Logs | StackOverflowError | Error traces, triggering execution paths |
Note:Thread dump analysis is critical for diagnosing both stack depth failures and thread capacity exhaustion, as it exposes call hierarchies and overall thread volume within the JVM. The following walkthrough demonstrates how thread dumps are captured and analyzed in real-world troubleshooting scenarios.
StackOverflowError vs Native Thread OOM: Understanding Thread Stack Failures in the JVM
While StackOverflowError and OutOfMemoryError: Unable to Create New Native Threads take different operational forms at different runtime levels, they both stem from the same operational boundary: thread stack memory.
One lies behind a depth-driven failure when a thread runs too many methods on the stack, or runs a method recursively.
The second is a capacity-driven failure: the situation where the JVM cannot provide native memory for any future thread stacks.
As these errors occur outside the Java heap, traditional memory diagnostics (for GC, heap, etc.) are incapable of surfacing a root cause. Instead, troubleshooting must have visibility into thread behavior, stack allocation, and native memory limits. By combining controlled reproduction with targeted diagnostics and governed thread design, engineering teams are able to mitigate both risks proactively.
JVM reliability is not only a question of heap tuning but also depends on the architectural complexity of thread execution depth and concurrency scale.

Share your Thoughts!