概要
OutOfMemoryError はアプリケーションの安定稼働を脅かす深刻な問題ですが、発生してから慌てて対処するケースが多いのが実情です。原因は大きく分けて、ヒープの枯渇(大量オブジェクトの蓄積)、メモリリーク(static フィールドへの無限追加)、スタックオーバーフロー(終了条件のない再帰)の3パターンに分類できます。この記事では、それぞれのパターンを最小限のコードで再現し、発生メカニズムを確認したうえで、WeakHashMap による自動解放や再帰のループ変換といった具体的な回避策を示します。また、OOM 発生時にヒープダンプを自動取得する JVM フラグの設定方法も扱います。現場で OOM に遭遇したときに、原因の切り分けと初動対応を迷わず進められることを目指します。
使いどころ
本番環境で OOM が発生した際に、ヒープダンプを自動取得して Eclipse MAT で原因を特定する
static Map にキャッシュデータを追加し続けるコードを WeakHashMap や容量上限付き LRU に置き換えてリークを防ぐ
再帰処理で StackOverflowError が出た箇所をループに書き換えて安定動作させる
コード例
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
public class OutOfMemoryDemo {
/**
* 実行すると OutOfMemoryError: Java heap space が発生。
* 再現: java -Xmx64m OutOfMemoryDemo
*/
public static void heapExhaustion() {
var list = new ArrayList<byte[]>();
while (true) {
list.add(new byte[1024 * 1024]); // 1MB ずつ追加
}
}
private static final Map<String, byte[]> CACHE = new HashMap<>();
/** static Map に追加し続けると GC が解放できず OOM になる */
public static void addToCache(String key, byte[] data) {
CACHE.put(key, data);
}
private static final WeakHashMap<Object, byte[]> WEAK_CACHE
= new WeakHashMap<>();
/** キーへの強参照がなくなれば GC がエントリを削除する */
public static void cacheWeakly(Object key, byte[] data) {
WEAK_CACHE.put(key, data);
}
/** 終了条件なしの再帰 -> StackOverflowError */
public static int infiniteRecursion(int n) {
return infiniteRecursion(n + 1);
}
/** 再帰をループに書き換えた安全な実装 */
public static long sumUpTo(long n) {
long result = 0;
for (long i = 1; i <= n; i++) {
result += i;
}
return result;
}
public static List<String> recommendedFlags() {
return List.of(
"-Xmx256m",
"-XX:+HeapDumpOnOutOfMemoryError",
"-XX:HeapDumpPath=/tmp/dump.hprof"
);
}
public static void main(String[] args) {
// メモリ使用状況の表示
var rt = Runtime.getRuntime();
System.out.println("最大ヒープ: " + rt.maxMemory() / 1024 / 1024 + " MB");
System.out.println("空きヒープ: " + rt.freeMemory() / 1024 / 1024 + " MB");
// 推奨 JVM フラグ
System.out.println("\n=== 推奨 JVM フラグ ===");
recommendedFlags().forEach(System.out::println);
// StackOverflowError の安全なデモ
System.out.println("\n=== StackOverflowError を安全に捕捉 ===");
try {
infiniteRecursion(0);
} catch (StackOverflowError e) {
System.out.println("StackOverflowError を捕捉しました");
}
// ループ実装(安全)
System.out.println("sumUpTo(100) = " + sumUpTo(100));
}
}Version Coverage
record でセッションデータ等の値オブジェクトを定義すると、メモリリークのデモがより実務的になる。var で変数宣言が簡潔になる。
// Java 17: record でリーク対象を型安全に表現
record SessionData(String userId, byte[] payload) {}
private static final Map<String, SessionData> SESSIONS
= new HashMap<>();
public static void register(String id, byte[] data) {
SESSIONS.put(id, new SessionData(id, data));
// 削除漏れがあると OOM の原因になる
}Library Comparison
注意点
OutOfMemoryError は Error のサブクラスであり、通常の業務コードで catch してはいけない。catch しても JVM の状態が不安定なため、安全な後処理は保証されない
StackOverflowError も Error であり、catch して握りつぶすと原因の特定が困難になる。デモ以外では catch しないこと
WeakHashMap はキーへの強参照がなくなったときに GC がエントリを回収する。文字列リテラルをキーにすると定数プールに保持されて GC されないため効果がない
-Xmx を大きくすれば OOM を回避できるとは限らない。メモリリークが原因の場合は GC 頻度が増えて応答性能が悪化し、最終的にはやはり OOM に至る
ヒープダンプファイル(.hprof)はヒープサイズと同程度の容量になる。ディスク容量が不足するとダンプが不完全になるため、HeapDumpPath の指定先に十分な空きを確保すること
FAQ
まず -XX:+HeapDumpOnOutOfMemoryError が設定されているか確認し、ヒープダンプを取得します。取得できたら Eclipse MAT で支配ツリー(Dominator Tree)を確認し、大量にメモリを占有しているオブジェクトを特定してください。
メモリリークが原因の場合、ヒープを増やしても発生が遅れるだけで根本解決にはなりません。リークの原因(static Map への無限追加、クローズ漏れ等)を特定して修正することが必要です。
再帰の深さが入力サイズに比例する場合はループに書き換えるのが安全です。Java は末尾再帰最適化(TCO)を行わないため、再帰が深くなる処理は原則としてループで実装してください。