Java の java.lang.StackOverflowError を修正する方法

beginner Java2026-03-17| Java 8以降、JVM(Windows / Linux / macOS)、再帰または深いコールチェーンを使用するすべての Java アプリケーション

Error Message

java.lang.StackOverflowError
#java#再帰#スタック

エラーの内容

Java アプリケーションを実行すると、JVM が次のエラーをスローします:

Exception in thread "main" java.lang.StackOverflowError
	at com.example.MyClass.calculate(MyClass.java:12)
	at com.example.MyClass.calculate(MyClass.java:12)
	at com.example.MyClass.calculate(MyClass.java:12)
	... (何百回も繰り返される)

同じ行が何百回も繰り返されています。これがサインです — コードが終了する前に JVM がコールスタックの領域を使い果たしたということです。

なぜ発生するのか

Java のメソッド呼び出しはそれぞれ独自のスタックフレームを持ちます。JVM はスレッドごとにこのためのメモリを固定量(通常 512KB ~ 1MB)確保します。呼び出しが巻き戻されないまま深くネストされると、その領域はすぐに満杯になります。

最もよく見られる原因は次の3つです:

  • ベースケースの欠落または誤り(再帰メソッドの基底条件)
  • 相互再帰 — メソッド A がメソッド B を呼び出し、メソッド B がメソッド A を呼び出す
  • 意図しない自己呼び出し — ゲッターが自身を呼び出したり、toString() が別の toString() をトリガーする

修正1 — ベースケースを修正する

スタックトレース内で繰り返されている行を見つけ、そのメソッドを開いて、停止条件があるかどうかを確認します。

問題のあるコード:

public int factorial(int n) {
    return n * factorial(n - 1);  // ベースケースなし — 永遠に実行される
}

修正後:

public int factorial(int n) {
    if (n  stack = new ArrayDeque<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode node = stack.pop();
        System.out.println(node.val);
        if (node.right != null) stack.push(node.right);
        if (node.left != null) stack.push(node.left);
    }
}

修正3 — 意図しない自己呼び出しを探して修正する

これは意外な落とし穴です。フィールドの代わりに自身を参照する toString() は永遠にループします:

// バグ: toString が自身を呼び出している
@Override
public String toString() {
    return "User: " + toString();  // this.name であるべきところがメソッドを呼び出している
}

// 修正:
@Override
public String toString() {
    return "User: " + this.name;
}

また、循環参照を持つエンティティに対して Lombok または IDE が生成した equals()/hashCode() にも注意してください — これは双方向 JPA リレーションシップでよく見られます。サイクルを断ち切るには、バック参照フィールドに @ToString.Exclude または @EqualsAndHashCode.Exclude を付けてください。

修正4 — スタックサイズを増やす(最終手段)

今すぐリファクタリングできませんか?-Xss フラグで時間を稼ぐことができます。デフォルトは通常 512k または 1m です — 4倍にしてみてください:

java -Xss4m -jar your-app.jar

特定のスレッドに大きなスタックが必要ですか?Thread コンストラクターで設定します:

Thread thread = new Thread(null, () -> {
    runDeepRecursion();
}, "deep-thread", 4 * 1024 * 1024);  // 4MB スタック
thread.start();

これは根本的な修正ではありません。入力が十分に大きければ、エラーは再発します。本当の修正に取り組む間の一時的な措置として使用してください — 代わりではありません。

確認手順

修正を適用したら、次の確認を行ってください:

  • アプリをクラッシュさせた入力を再度実行します。エラーが消えているはずです。
  • エッジケースをテストします:n = 0n = 1、負の値、空のリスト。
  • 反復バージョンに大きな入力を渡してみます — n = 100000 を試して — 問題ないことを確認します。
  • ユニットテストで固定して、リグレッションを防ぎます:
@Test
public void testFactorialLargeInput() {
    // StackOverflowError をスローしないはず
    assertDoesNotThrow(() -> factorial(10000));
}

予防のヒント

  • ベースケースを最初に、メソッドの先頭に、再帰呼び出しの前に記述してください。
  • ユーザーが制御する入力の場合、再帰の深さを明示的に制限します:
public void traverse(Node node, int depth) {
    if (node == null || depth > 1000) return;
    traverse(node.next, depth + 1);
}
  • グラフやツリーはサイクルを持つことがあります。訪問済みのノードを Set で追跡して、無限ループを防いでください。
  • コードレビューでは、すべての再帰メソッドをリスクとして扱ってください — ベースケースが正しいか、実際に到達できるかを確認してください。

Related Error Notes