エラーの内容
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 = 0、n = 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で追跡して、無限ループを防いでください。 - コードレビューでは、すべての再帰メソッドをリスクとして扱ってください — ベースケースが正しいか、実際に到達できるかを確認してください。

