P-03: OutOfMemoryError を体験する
OutOfMemoryError(OOM)は Java の本番トラブルで最も深刻な部類に入ります。 原因は「ヒープ枯渇」「メモリリーク」「スタックオーバーフロー」の3パターンに分類できます。 それぞれの再現方法・対策・ヒープダンプ取得方法を解説します。
3 つのパターンと対策
① ヒープ枯渇(Java heap space)
new byte[1024 * 1024] を繰り返すと JVM のヒープ領域が不足し OOM になります。 対策: -Xmx でヒープを増やす、または大きいデータをストリームで分割処理する。
② メモリリーク(static フィールドへの無限追加)
static Map にオブジェクトを追加し続けると、GC が解放できず徐々にメモリを食い潰します。 対策: WeakHashMap、容量上限付き LRU キャッシュ、TTL 管理を導入する。
③ StackOverflowError(無限再帰)
終了条件のない再帰呼び出しはスタックを使い切り StackOverflowError になります。 Java は末尾再帰最適化(TCO)をしないため、深い再帰はループに書き換えてください。
OOM 発生時の推奨 JVM フラグ
# OOM 発生時にヒープダンプを自動取得する -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/app/heapdump.hprof # ヒープサイズの設定(環境に合わせて調整) -Xms256m # 初期ヒープ -Xmx1g # 最大ヒープ # ダンプ分析ツール: Eclipse MAT / VisualVM / JProfiler
サンプルコード
⚠️ heapExhaustion() を呼ぶと JVM がクラッシュします。 必ず java -Xmx64m など小さいヒープ上限を設定してから実行してください。
よくあるミス・注意点
⚠️ WeakHashMap は「キーが常に保持されている」場合は効果なし
WeakHashMap のエントリは「キーへの強参照がなくなった時」に解放されます。 キーとして文字列リテラル(インターンされた文字列)を使うと、 プログラムが終了するまで解放されません。キーのライフサイクルを意識して使ってください。
⚠️ OutOfMemoryError は通常の catch で捕捉しない
OutOfMemoryError は Error のサブクラスです。catch (Exception e) では捕捉できません。 捕捉しても JVM のメモリが足りない状態は変わらないため、通常は捕捉せずに JVM を再起動する運用を取ります。
⚠️ ヒープダンプなしで OOM を調査するのは困難
本番環境では必ず -XX:+HeapDumpOnOutOfMemoryError を設定しておきましょう。 OOM 発生後に「どのクラスがどれだけメモリを占有していたか」を確認するには ヒープダンプが不可欠です。設定なしに発生した OOM は調査が非常に困難になります。
テストする観点
- ✅
infiniteRecursion()がStackOverflowErrorを発生させるか - ✅
safeSum(100)が 5050 を返すか(ループ実装の正確性) - ✅
WeakHashMapに追加後、キーを null にして GC を促したとき、エントリが削除されるか - ✅
-Xmx32mでheapExhaustion()を呼ぶとOutOfMemoryErrorが発生するか(JVM フラグのテスト)