java-recipes

ホーム 並行処理・メモリ › P-03

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 など小さいヒープ上限を設定してから実行してください。

Sample.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * OutOfMemoryError / StackOverflowError の再現と回避パターン(Java 8+)。
 *
 * ⚠️ heapExhaustion() を呼ぶと JVM がクラッシュします。
 *    必ず -Xmx64m など小さいヒープで実行してください。
 */
public class OutOfMemorySample {

    // ---- パターン1: ヒープ枯渇 ----
    // java -Xmx64m OutOfMemorySample で再現

    public static void heapExhaustion() {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[1024 * 1024]); // 1MB ずつ追加
        }
        // → OutOfMemoryError: Java heap space
    }

    // ---- パターン2: メモリリーク(static Map への無限追加)----
    //
    // static フィールドはGCのルートになるため、中のオブジェクトが解放されない。
    // セッションやキャッシュをむやみに static に保持すると OOM になる。

    private static final Map<String, byte[]> CACHE = new HashMap<>();

    public static void addToCache(String key, byte[] data) {
        CACHE.put(key, data); // 削除されないと溜まり続ける
    }

    // ---- パターン3: WeakHashMap によるリーク回避 ----
    //
    // WeakHashMap はキーへの強参照がなくなると GC が自動的にエントリを削除する。
    // ただし「キーが常に保持されている」場合は効果がない点に注意。

    private static final WeakHashMap<String, byte[]> WEAK_CACHE = new WeakHashMap<>();

    public static void addToWeakCache(String key, byte[] data) {
        WEAK_CACHE.put(key, data);
    }

    // ---- パターン4: StackOverflowError(終了条件のない再帰)----

    public static int infiniteRecursion(int n) {
        return infiniteRecursion(n + 1); // 終了条件なし → StackOverflowError
    }

    // 対策: 再帰をループに書き換える(Java は末尾再帰最適化=TCO 非対応)
    public static long safeSum(long n) {
        long result = 0;
        for (long i = 1; i <= n; i++) {
            result += i;
        }
        return result;
    }

    // ---- JVM フラグ ----
    //
    // 本番では以下を設定してヒープダンプを自動取得すること:
    //   -Xmx512m
    //   -XX:+HeapDumpOnOutOfMemoryError
    //   -XX:HeapDumpPath=/var/log/app/heapdump.hprof

    public static void main(String[] args) {
        System.out.println("=== 推奨 JVM フラグ ===");
        System.out.println("-Xmx512m");
        System.out.println("-XX:+HeapDumpOnOutOfMemoryError");
        System.out.println("-XX:HeapDumpPath=/var/log/app/heapdump.hprof");

        // StackOverflowError を安全にデモ(try-catch で捕捉)
        System.out.println("\n=== StackOverflowError デモ ===");
        try {
            infiniteRecursion(0);
        } catch (StackOverflowError e) {
            System.out.println("StackOverflowError を捕捉しました");
        }
        // ※ StackOverflowError は Error のサブクラス。
        //   業務コードでは通常 catch しないこと。

        System.out.println("\n=== 安全なループ実装 ===");
        System.out.println("safeSum(100) = " + safeSum(100)); // 5050
    }
}

よくあるミス・注意点

⚠️ WeakHashMap は「キーが常に保持されている」場合は効果なし

WeakHashMap のエントリは「キーへの強参照がなくなった時」に解放されます。 キーとして文字列リテラル(インターンされた文字列)を使うと、 プログラムが終了するまで解放されません。キーのライフサイクルを意識して使ってください。

⚠️ OutOfMemoryError は通常の catch で捕捉しない

OutOfMemoryErrorError のサブクラスです。catch (Exception e) では捕捉できません。 捕捉しても JVM のメモリが足りない状態は変わらないため、通常は捕捉せずに JVM を再起動する運用を取ります。

⚠️ ヒープダンプなしで OOM を調査するのは困難

本番環境では必ず -XX:+HeapDumpOnOutOfMemoryError を設定しておきましょう。 OOM 発生後に「どのクラスがどれだけメモリを占有していたか」を確認するには ヒープダンプが不可欠です。設定なしに発生した OOM は調査が非常に困難になります。

テストする観点

  • infiniteRecursion()StackOverflowError を発生させるか
  • safeSum(100) が 5050 を返すか(ループ実装の正確性)
  • WeakHashMap に追加後、キーを null にして GC を促したとき、エントリが削除されるか
  • -Xmx32mheapExhaustion() を呼ぶと OutOfMemoryError が発生するか(JVM フラグのテスト)

GitHub でソースコードを見る →