こんにちは、かつコーチです。
「例外(Exception)は分かるけど、Errorが出るとどう対処していいか分からない」という声をよく聞きます。
今回は代表的な2つのError、StackOverflowErrorとOutOfMemoryErrorの原因と対処法を整理します。
ExceptionとErrorの違い
回復可能性で分かれる階層
Javaの例外クラスはThrowableを頂点に、Exception系とError系に分かれています。Exceptionはプログラムが対処して回復できる問題を表すのに対し、ErrorはJVM(Java仮想マシン、Javaプログラムを実行する土台となるソフトウェア)レベルの深刻な問題で、基本的にアプリケーション側での回復が期待されていません。
StackOverflowErrorとOutOfMemoryErrorはどちらもメモリに関する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, 中級者向け, エラー解決