StackOverflowError means one thread exhausted the memory reserved for nested method-call frames.
The repeating frames near the top of the stack trace usually reveal infinite recursion, mutual recursion, or an unexpectedly deep input.
Fix the termination or depth model first; increasing -Xss only changes the limit and can reduce how many threads a process can support.
Each Java thread has a call stack. A method invocation adds a frame containing return state and local execution data; returning removes that frame. StackOverflowError is thrown when another frame cannot be created. It is an Error rather than a normal recoverable application exception because execution has reached a JVM resource boundary.
Recursion is not automatically faulty. A recursive method is safe only when every path has a reachable base case and the maximum depth fits supported input. A balanced tree may have logarithmic depth, while a degenerate tree with the same node count can produce one frame per node.
// Missing a stopping condition.
int brokenFactorial(int n) {
return n * brokenFactorial(n - 1);
}
// The recursive argument moves toward a reachable base case.
int factorial(int n) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
if (n <= 1) return 1;
return n * factorial(n - 1);
}
long factorialIterative(int n) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
long result = 1;
for (int i = 2; i <= n; i++) result *= i;
return result;
}
factorial(5) and factorialIterative(5) both return 120; brokenFactorial eventually throws StackOverflowError.
A valid base case is only half of the proof: the recursive argument must move toward it. The iterative version removes call-depth growth but still has numeric-overflow limits.
int sum(int n) {
return n + sum(n - 1); // Wrong No base case "-- infinite recursion!
}
Calling sum(1) eventually throws StackOverflowError.
The argument decreases forever because zero has no return path.
int sum(int n) {
if (n <= 0) return 0; // Correct Base case
return n + sum(n - 1);
}
sum(3) returns 6.
The base case handles zero and negative input before the next recursive call.
class Node {
Node next;
@Override
public String toString() {
return "Node{next=" + next + "}"; // Unsafe: calls next.toString(), infinite if circular.
}
}
A cyclic next link repeatedly formats the same nodes until the stack is exhausted.
Following next recursively is unsafe when a linked structure can contain a cycle.
class Node {
int value;
Node next;
@Override
public String toString() {
// Correct Don't recurse into next "-- just show value
return "Node{value=" + value + ", hasNext=" + (next != null) + "}";
}
}
Example: Node{value=7, hasNext=true}.
Printing only local state prevents toString from traversing an unbounded graph.
// Recursive Fibonacci (can overflow for large n)
int fibRecursive(int n) {
if (n <= 1) return n;
return fibRecursive(n - 1) + fibRecursive(n - 2);
}
// Correct Iterative Fibonacci (no stack overflow)
long fibIterative(int n) {
if (n <= 1) return n;
long a = 0, b = 1;
for (int i = 2; i <= n; i++) {
long temp = a + b;
a = b;
b = temp;
}
return b;
}
// Correct Or use explicit stack for tree traversal
void traverseIterative(TreeNode root) {
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
System.out.println(node.value);
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}
}
The iterative traversal prints each reachable node once when the input is an acyclic tree.
Iteration stores pending state explicitly and avoids one JVM call frame per input level.
Increasing the thread stack can postpone StackOverflowError, but it does not correct an algorithm whose depth grows without a safe bound. First prove that every recursive branch approaches a base case. Then estimate the maximum depth from the input. A recursive tree traversal whose depth equals the height may be reasonable for a balanced tree and unsafe for a degenerate one.
When depth can grow with untrusted or production-sized input, move pending work into a heap-backed collection. An explicit stack preserves depth-first order while making memory use visible and controllable. A queue provides breadth-first traversal when level order is acceptable. Both approaches also provide a natural place to enforce node, depth, or time limits.
Do not catch StackOverflowError and continue ordinary processing. The thread may have too little stack space for reliable recovery logic. Treat it as evidence that recursion bounds, cyclic data, or termination logic must be corrected, then restart the operation from a known state.
import java.util.ArrayDeque;
import java.util.Deque;
public class Main {
public static void main(String[] args) {
Deque<Integer> pending = new ArrayDeque<>();
pending.push(3);
while (!pending.isEmpty()) {
int value = pending.pop();
System.out.print(value + " " );
if (value > 0) pending.push(value - 1);
}
}
}
3 2 1 0
The deque stores pending work on the heap, so algorithm depth is no longer tied to one Java thread stack.
Start at the first application-owned frame and look for a repeating method or a short cycle of methods. A direct recursion trace repeats one method and line. Mutual recursion alternates methods such as parseExpression and parseGroup. Framework frames may surround the cycle, so identify the earliest point where application data or control re-enters the same path.
Preserve the complete trace and the input that triggered it. Logging only the error message discards the frames needed for diagnosis. If the trace is shortened by tooling, reproduce with an uncapped trace in a safe environment. Add input size, tree depth, request identity, and relevant configuration so a finite-but-excessive depth can be distinguished from a logic loop.
For infinite recursion, repair the control flow: add the missing base case, make progress toward it, or break a cycle with visited-object identity. For valid but input-dependent depth, replace recursion with an explicit Deque, enforce a supported depth limit, rebalance the data structure, or process it in chunks. Tail-recursive Java code still consumes frames because the Java language and JVM do not guarantee tail-call elimination.
The -Xss option changes the stack reservation per thread. A larger value may be justified for a measured, finite algorithm that cannot reasonably be rewritten, but it increases per-thread memory pressure and can reduce concurrency. Record the workload, recursion depth, JVM, architecture, and thread count when testing it. Never present a larger stack as the correction for non-terminating recursion.
Do not routinely catch StackOverflowError and continue the same operation. Even if a narrow boundary records the failure, the request state may be incomplete and recovery code itself needs stack. Correct the root cause, reject unsupported input predictably, and restart work from a known state.
Try this next
0 of 2 completed
It's caused by infinite recursion "" a method calling itself without a proper base case, or a base case that's never reached. Each method call adds a frame to the call stack, which eventually runs out of space.
Add or fix the base case in your recursive method. Verify the recursion actually moves toward the base case. Consider converting to an iterative solution for large inputs.
Yes, use the JVM flag -Xss: java -Xss4m MyProgram. But this is a workaround "" fixing the recursion logic is the proper solution.
Explore 500+ free tutorials across 20+ languages and frameworks.