【Java】StackOverflowErrorとOutOfMemoryErrorの原因と対処法

Java

こんにちは、かつコーチです。
「例外(Exception)は分かるけど、Errorが出るとどう対処していいか分からない」という声をよく聞きます。
今回は代表的な2つのErrorStackOverflowErrorOutOfMemoryErrorの原因と対処法を整理します。

ExceptionとErrorの違い

回復可能性で分かれる階層

Javaの例外クラスはThrowableを頂点に、Exception系とError系に分かれています。
Exceptionはプログラムが対処して回復できる問題を表すのに対し、ErrorはJVM(Java仮想マシン、Javaプログラムを実行する土台となるソフトウェア)レベルの深刻な問題で、基本的にアプリケーション側での回復が期待されていません。

StackOverflowErrorOutOfMemoryErrorはどちらもメモリに関するErrorですが、原因となるメモリ領域が異なります。

StackOverflowError:スタック領域の枯渇

原因は再帰の終了条件漏れ

StackOverflowErrorは、メソッド呼び出しの情報を積むスタック領域(メソッドの呼び出し履歴やローカル変数を保存する限られたメモリ領域)が上限を超えたときに発生します。
最も多い原因は、終了条件のない、または誤った再帰呼び出し(メソッドが自分自身を呼び出す処理)です。

// ❌Before:終了条件がない再帰メソッド
public static int factorial(int n) {
    return n * factorial(n - 1); // n=0になっても呼び出しが続く
}

public static void main(String[] args) {
    System.out.println(factorial(5));
}
Exception in thread "main" java.lang.StackOverflowError
        at Main.factorial(Main.java:2)
        at Main.factorial(Main.java:2)
        at Main.factorial(Main.java:2)
        at Main.factorial(Main.java:2)
        ... (同じ行が延々と繰り返される)

スタックトレースに同じメソッド・同じ行番号が延々と繰り返されているのがStackOverflowErrorの典型的な特徴です。

// ✅After:終了条件を明記する
public static int factorial(int n) {
    if (n <= 1) {
        return 1; // 終了条件
    }
    return n * factorial(n - 1);
}

対処のポイント

再帰を使う場合は、必ず「どの入力でループが止まるか」を最初に決めてからコードを書くようにしましょう。
また、再帰の深さが非常に大きくなることが想定される処理(大量データの走査など)は、for文やwhile文を使った反復処理への書き換えも検討する価値があります。

OutOfMemoryError:ヒープ領域の枯渇

原因は不要なオブジェクトの滞留

OutOfMemoryErrorは、オブジェクトの実体が確保されるヒープ領域(インスタンスや配列の実体が置かれるメモリ領域)が枯渇したときに発生します。

// ❌Before:ループのたびに大量データを保持し続ける
List<int[]> dataList = new ArrayList<>();
while (true) {
    int[] bigArray = new int[1_000_000];
    dataList.add(bigArray); // 参照を持ち続けるためGCで回収されない
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at Main.main(Main.java:4)

Java heap spaceというメッセージは、ヒープ領域そのものが尽きたことを示しています。
このコードでは、dataListが確保した配列への参照を持ち続けるため、ガベージコレクション(不要になったオブジェクトを自動で回収する仕組み、通称GC)が回収できず、メモリ使用量が増え続けてしまいます。

// ✅After:不要になった参照を明示的に解放する、または保持しない設計にする
for (int i = 0; i < 100; i++) {
    int[] bigArray = new int[1_000_000];
    process(bigArray); // その場で処理して保持しない
}

-Xmxオプションでヒープサイズを調整する

一時的な対処として、JVM起動時のオプションでヒープサイズの上限を増やすこともできます。

$ java -Xmx512m Main

ただし、これは根本解決ではなく応急処置です。
メモリ使用量が増え続ける設計自体を見直さない限り、いずれ同じ問題が再発します。

つまずいたポイント:ログ出力用リストが原因だった話

実務で、長時間稼働するバッチ処理が数時間後に必ずOutOfMemoryErrorで落ちるという不具合に遭遇しました。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.Arrays.copyOf(Arrays.java:3745)
        at java.base/java.util.ArrayList.grow(ArrayList.java:237)
        at java.base/java.util.ArrayList.add(ArrayList.java:485)
        at com.example.BatchProcessor.logResult(BatchProcessor.java:22)

原因を調べたところ、デバッグ用に「処理結果をすべてListに蓄積してから最後にまとめて出力する」という実装が残っており、処理件数が多い日にヒープを圧迫していました。

// ❌Before:処理結果を全件メモリに溜め込む
List<String> results = new ArrayList<>();
for (Order order : orders) {
    results.add(processOrder(order));
}
results.forEach(System.out::println);
// ✅After:処理と同時に出力し、メモリに溜め込まない
for (Order order : orders) {
    System.out.println(processOrder(order));
}

スタックトレースにArrayList.growが現れているのは、リストの拡張中にメモリが尽きたことを示す典型的なサインです。
「なぜためているのか」を見直すだけで解決するケースは実務でも意外と多くあります。

まとめ

この記事のポイント

  • StackOverflowErrorはスタック領域の枯渇で、終了条件のない再帰が主な原因
  • 同じメソッド・行番号が延々と続くスタックトレースは再帰の終了条件漏れのサイン
  • OutOfMemoryErrorはヒープ領域の枯渇で、不要なオブジェクトを保持し続けることが主な原因
  • -Xmxによるヒープサイズ調整は応急処置であり、根本原因の設計見直しが必要

次に読むべき記事

  • スタックトレースの読み解き方を深めたい方は「デバッグの基本(IDEのブレークポイント)」の記事へ
  • コレクションの基礎知識は「Listの基本:ArrayListとLinkedListの違い」の記事へ

タグ: Java, 中級者向け, エラー解決

タイトルとURLをコピーしました