概要

OutOfMemoryError はアプリケーションの安定稼働を脅かす深刻な問題ですが、発生してから慌てて対処するケースが多いのが実情です。原因は大きく分けて、ヒープの枯渇(大量オブジェクトの蓄積)、メモリリーク(static フィールドへの無限追加)、スタックオーバーフロー(終了条件のない再帰)の3パターンに分類できます。この記事では、それぞれのパターンを最小限のコードで再現し、発生メカニズムを確認したうえで、WeakHashMap による自動解放や再帰のループ変換といった具体的な回避策を示します。また、OOM 発生時にヒープダンプを自動取得する JVM フラグの設定方法も扱います。現場で OOM に遭遇したときに、原因の切り分けと初動対応を迷わず進められることを目指します。

使いどころ

本番環境で OOM が発生した際に、ヒープダンプを自動取得して Eclipse MAT で原因を特定する

static Map にキャッシュデータを追加し続けるコードを WeakHashMap や容量上限付き LRU に置き換えてリークを防ぐ

再帰処理で StackOverflowError が出た箇所をループに書き換えて安定動作させる

コード例

OutOfMemoryDemo.java
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));
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でセッションデータ等の値オブジェクトを定義すると、メモリリークのデモがより実務的になる。var で変数宣言が簡潔になる。

Java 17
// 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

標準 API(WeakHashMap / Runtime)メモリリークの回避や使用量の簡易モニタリング。追加依存なしで基本的な対策ができる。WeakHashMap はキーの参照管理が必要。本格的なキャッシュには容量制限やTTLの仕組みが不足する。
Caffeine容量上限・TTL・統計情報付きの高性能キャッシュが必要なとき。依存追加が必要。小規模なキャッシュには過剰な場合がある。
Eclipse MAT / VisualVMヒープダンプを解析して OOM の原因オブジェクトを特定するとき。ライブラリではなく解析ツール。コードに組み込むものではないが、OOM 調査には不可欠。

注意点

OutOfMemoryError は Error のサブクラスであり、通常の業務コードで catch してはいけない。catch しても JVM の状態が不安定なため、安全な後処理は保証されない

StackOverflowError も Error であり、catch して握りつぶすと原因の特定が困難になる。デモ以外では catch しないこと

WeakHashMap はキーへの強参照がなくなったときに GC がエントリを回収する。文字列リテラルをキーにすると定数プールに保持されて GC されないため効果がない

-Xmx を大きくすれば OOM を回避できるとは限らない。メモリリークが原因の場合は GC 頻度が増えて応答性能が悪化し、最終的にはやはり OOM に至る

ヒープダンプファイル(.hprof)はヒープサイズと同程度の容量になる。ディスク容量が不足するとダンプが不完全になるため、HeapDumpPath の指定先に十分な空きを確保すること

FAQ

OutOfMemoryError が出たらまず何をすべきですか。

まず -XX:+HeapDumpOnOutOfMemoryError が設定されているか確認し、ヒープダンプを取得します。取得できたら Eclipse MAT で支配ツリー(Dominator Tree)を確認し、大量にメモリを占有しているオブジェクトを特定してください。

-Xmx を増やすだけでは解決しないのですか。

メモリリークが原因の場合、ヒープを増やしても発生が遅れるだけで根本解決にはなりません。リークの原因(static Map への無限追加、クローズ漏れ等)を特定して修正することが必要です。

再帰を使いたいが StackOverflowError が心配です。どう対策すべきですか。

再帰の深さが入力サイズに比例する場合はループに書き換えるのが安全です。Java は末尾再帰最適化(TCO)を行わないため、再帰が深くなる処理は原則としてループで実装してください。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます