Definition
- StackOverflowError = the per-thread call stack (default 512KB to 1MB) is exhausted. Each method call pushes a frame. Causes: unbounded recursion, mutual recursion, cyclic toString/equals.
- OutOfMemoryError = the shared JVM heap (default 256MB to 4GB) is full. Causes: memory leaks, loading all records into memory, classloader leaks.
- Key difference: SOE is per-thread (stack depth). OOM is JVM-wide (heap space). SOE is usually a code bug. OOM is usually a memory leak or undersized heap.
StackOverflowError: a teller at National Bank of India keeps opening sub-requests inside sub-requests (recursion) until their desk collapses under the paperwork. Each teller has a small desk (512KB stack). OutOfMemoryError: the entire bank vault (shared heap) is full with too many safe deposit boxes. SOE affects one desk. OOM affects the whole bank.
Understanding the Concepts
What causes StackOverflowError?
The most common cause is unbounded recursion where the base case is missing or wrong.
package com.bank.demo;
public class StackOverflowDemo {
// CAUSE 1: missing base case
public static int wrongFactorial(int n) {
return n * wrongFactorial(n - 1); // never stops!
}
// FIX: add base case
public static int correctFactorial(int n) {
if (n <= 1) return 1; // base case stops recursion
return n * correctFactorial(n - 1);
}
// CAUSE 2: cyclic toString in JPA entities
// class Account { List<Trade> trades; toString() { return trades.toString(); } }
// class Trade { Account account; toString() { return account.toString(); } }
// Account.toString() -> Trade.toString() -> Account.toString() -> infinite!
// FIX: exclude parent reference in toString
public static void main(String[] args) {
System.out.println("Correct factorial(5): " + correctFactorial(5));
try {
wrongFactorial(100000);
} catch (StackOverflowError e) {
System.out.println("SOE caught: stack exhausted by recursion");
}
}
}
Correct factorial(5): 120
SOE caught: stack exhausted by recursion
How is SOE different from OOM?
| Feature | StackOverflowError | OutOfMemoryError |
|---|---|---|
| Memory area | Per-thread stack (512KB-1MB) | Shared heap (256MB-4GB) |
| Cause | Too many stack frames (depth) | Too many objects (space) |
| Common trigger | Recursion bug | Memory leak, large data |
| Scope | One thread dies | JVM-wide, everything affected |
| Fix | Base case, iteration, -Xss | Fix leak, -Xmx, profiler |
| Recoverable? | Sometimes (thread is gone) | Usually fatal |
package com.bank.demo;
public class SOEvsOOMDemo {
public static void main(String[] args) {
System.out.println("SOE: per-thread stack, caused by recursion");
System.out.println("OOM: shared heap, caused by memory leak");
System.out.println();
System.out.println("OOM variants:");
System.out.println(" Java heap space: heap full");
System.out.println(" Metaspace: too many classes loaded");
System.out.println(" GC overhead limit: GC runs >98% time, recovers <2%");
System.out.println(" native thread: OS thread limit reached");
}
}
SOE: per-thread stack, caused by recursion
OOM: shared heap, caused by memory leak
OOM variants:
Java heap space: heap full
Metaspace: too many classes loaded
GC overhead limit: GC runs >98% time, recovers <2%
native thread: OS thread limit reached
What should a senior engineer know?
- JPA bidirectional. Always use
@ToString.Exclude(Lombok) or manual toString excluding parent references. This prevents SOE in logging. - -Xss flag. Increases per-thread stack size but costs memory for ALL threads. Use sparingly.
- -XX:+HeapDumpOnOutOfMemoryError. Generates a heap dump on OOM for analysis with Eclipse MAT.
- Tail call optimization. Java does not support TCO. Deep recursion should be converted to iteration.
What are common interview follow-up questions?
- What is the default thread stack size? (Typically 512KB to 1MB depending on OS and JVM.)
- Can you catch StackOverflowError? (Yes, but the thread's stack is in a fragile state.)
- What is the difference between -Xss and -Xmx? (-Xss is per-thread stack, -Xmx is heap.)
- How do you debug OOM? (Heap dump with -XX:+HeapDumpOnOutOfMemoryError, analyze with MAT/VisualVM.)
What are common mistakes?
A common mistake is not understanding the concept fully before using it in production banking applications.
Wrong approach:
package com.bank.service;
public class TransferService {
public void transfer(String fromAccount, String toAccount, double amount) {
// WRONG: using double for money
double balance = getBalance(fromAccount);
balance = balance - amount;
}
private double getBalance(String id) { return 10000.0; }
}
Correct approach:
package com.bank.service;
import java.math.BigDecimal;
public class TransferService {
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
// CORRECT: using BigDecimal for money
BigDecimal balance = getBalance(fromAccount);
balance = balance.subtract(amount);
}
private BigDecimal getBalance(String id) { return new BigDecimal("10000.00"); }
}
Always use BigDecimal for monetary calculations in banking systems to avoid floating-point precision errors.
What are the edge cases?
Edge cases can cause unexpected behavior in banking applications. Here are some to watch for.
package com.bank.edge;
public class EdgeCaseDemo {
public static void main(String[] args) {
// Edge case 1: null handling
String accountId = null;
try {
System.out.println(accountId.length());
} catch (NullPointerException e) {
System.out.println("Null account ID");
}
// Edge case 2: empty vs null
String emptyId = "";
System.out.println(emptyId.isEmpty()); // true
System.out.println(emptyId == null); // false
// Edge case 3: boundary values
java.math.BigDecimal zero = java.math.BigDecimal.ZERO;
java.math.BigDecimal negativeBalance = new java.math.BigDecimal("-0.01");
System.out.println(zero.compareTo(negativeBalance) > 0); // true
}
}
Null account ID
true
false
true
Always test with null values, empty strings, boundary values, and negative amounts in banking applications.
Visual Overview
StackOverflowError: OutOfMemoryError: ┌──────────────────┐ ┌──────────────────────┐ │ Thread Stack │ │ JVM Heap │ │ (per thread) │ │ (shared) │ │ ~512KB-1MB │ │ ~256MB-4GB │ │ │ │ │ │ frame(n) │ │ Object Object Object │ │ frame(n-1) │ │ Object Object Object │ │ frame(n-2) │ │ Object ... full ... │ │ ... overflow ... │ │ No room for new obj │ └──────────────────┘ └──────────────────────┘ Fix: base case, Fix: fix leak, iteration, -Xss -Xmx, profiler
Key Takeaway
StackOverflowError occurs when the per-thread call stack is exhausted by too many nested method calls. The most common causes are unbounded recursion (missing base case), mutual recursion (A calls B calls A), and cyclic toString/equals in JPA entities with bidirectional relationships. OutOfMemoryError occurs when the shared JVM heap is full from memory leaks or loading too much data into memory.
The key difference: SOE is per-thread (one thread dies, others continue) while OOM is JVM-wide (everything is affected). SOE is usually a code bug fixed by adding a base case or converting to iteration. OOM is usually a memory leak fixed by profiling with heap dumps.
At National Bank of India, the trade processing service had a cyclic toString between Account and Trade entities. Logging a trade triggered Account.toString() which called Trade.toString() which called Account.toString() in an infinite loop. Fix: exclude the parent reference with @ToString.Exclude in Lombok.
Remember one sentence: StackOverflowError is per-thread recursion exhaustion while OutOfMemoryError is JVM-wide heap exhaustion.
---