Back to Common Runtime Exceptions
Q1Question 1 of 4

What causes StackOverflowError?

Key Concepts
  • StackOverflowError occurs when the per-thread call stack (512KB to 1MB) is exhausted by too many nested method calls
  • Common causes: unbounded recursion (missing base case), mutual recursion (A calls B calls A), cyclic toString/equals in JPA entities
  • OutOfMemoryError occurs when the shared JVM heap is full, caused by memory leaks or large data loads
  • SOE is per-thread (one thread dies), OOM is JVM-wide (everything affected)
  • Fix SOE: add base case, convert recursion to iteration, exclude parent references in JPA toString
  • Fix OOM: profile with heap dumps, fix memory leaks, increase -Xmx, use streaming/pagination

Definition

StackOverflowError is caused by exhausting the per-thread call stack through unbounded recursion, while OutOfMemoryError is caused by exhausting the shared JVM heap through memory leaks or large data.
  • 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.

JAVA
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");
        }
    }
}
TEXT
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 |

JAVA
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");
    }
}
TEXT
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:

JAVA
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:

JAVA
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.

JAVA
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
    }
}
TEXT
Null account ID
true
false
true

Always test with null values, empty strings, boundary values, and negative amounts in banking applications.

Visual Overview

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate