JVMメモリオーバーフローの原因と対策:Metaspace・スタック・ヒープの実践的診断

JVMにおける主なメモリ枯渇シナリオ

Javaアプリケーションで発生するOutOfMemoryError(OOM)は、通常以下の3つの領域で観測されます:
  • Metaspace領域:クラス定義やメタデータの格納場所
  • Java仮想マシンスタック:各スレッドの実行コンテキストを保持
  • ヒープ領域:インスタンスオブジェクトと配列の動的確保領域

Metaspace領域の枯渇

デフォルトでは、Metaspaceは無制限に拡張可能ですが、明示的にサイズ制限を設けると容易に枯渇します。典型的な設定例:
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m

主な原因:

  • 動的クラス生成ライブラリ(例:CGLIB、Byte Buddy)による過剰なクラスロード(特にキャッシュ無効化時)
  • クラスローダーのリーク(同一クラスの重複ロードや未解放参照)

再現コード(CGLIBベース):

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

public class MetaspaceLeakDemo {
    public static void main(String[] args) {
        int count = 0;
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Worker.class);
            enhancer.setUseCache(false); // キャッシュ無効 → 毎回新規クラス生成
            enhancer.setCallback((MethodInterceptor) (obj, method, argsArray, proxy) -> {
                System.out.println("Intercepted: " + method.getName());
                return proxy.invokeSuper(obj, argsArray);
            });
            Worker worker = (Worker) enhancer.create();
            worker.process();
            if (++count % 100 == 0) {
                System.out.println("Generated " + count + " dynamic classes");
            }
        }
    }

    static class Worker {
        public void process() { /* 処理内容 */ }
    }
}

実行時に-XX:MetaspaceSize=32m -XX:MaxMetaspaceSize=32mを指定すると、数秒以内にjava.lang.OutOfMemoryError: Metaspaceが発生します。

スタック領域の枯渇

JVMスレッドごとに割り当てられるスタック領域は、再帰呼び出しや極端に深いネスト呼び出しで簡単に消費されます。

デフォルト値と調整:

  • Linux/x64環境ではデフォルト約1MB(-Xss1m)
  • 高並列スレッド数が必要な場合、-Xss512kなど適宜縮小可能(ただし過度な縮小はリスク)

再現コード(無限再帰):

public class StackOverflowDemo {
    private static int depth = 0;

    public static void main(String[] args) {
        try {
            recurse();
        } catch (StackOverflowError e) {
            System.err.println("Stack overflow at depth: " + depth);
        }
    }

    private static void recurse() {
        depth++;
        recurse(); // 終了条件なし → スタック枯渇
    }
}

実行時引数:-Xss256kで早期にjava.lang.StackOverflowErrorを誘発できます。

ヒープ領域の枯渇

最も頻発するOOMタイプであり、主に以下のようなパターンで発生します:
  • 短期間での大量オブジェクト生成(例:バッチ処理中の一括リスト構築)
  • 参照保持によるGC回避(静的コレクションへの不適切な追加、コールバック登録後の未解除など)
  • 外部サービス障害時のバックプレッシャー(Kafka接続失敗時のメッセージ蓄積など)

再現コード(ヒープ飽和):

import java.util.*;

public class HeapSaturationDemo {
    public static void main(String[] args) {
        List buffers = new ArrayList<>();
        long counter = 0;
        while (true) {
            buffers.add(new byte[1024 * 1024]); // 1MB配列を追加
            if (++counter % 100 == 0) {
                System.out.println("Allocated " + counter + " MB");
            }
        }
    }
}

実行時引数:-Xms64m -Xmx64mで数秒でjava.lang.OutOfMemoryError: Java heap spaceを再現可能です。

実運用における典型事例と対応

Kafka連携障害によるメモリ圧迫

リアルタイム処理システムで、Kafkaプロデューサーの送信失敗時に指数バックオフ付きリトライ+結果保持を実装していた場合、Kafkaクラスタ障害時に送信不能なメッセージがメモリ上に蓄積し続けます。さらに、継続的な入力データ処理も並行して行われるため、ヒープ使用率が急上昇します。

緩和策:

  • 送信失敗時にローカルディスクへの退避(例:RocksDBまたはシリアライズドファイル)
  • 最大保持件数/最大保持時間による制限(TTL付きキュー)
  • 非同期送信におけるバックプレッシャー伝搬(例:ReactorのonBackpressureBuffer制御)

ESログ出力における再帰呼び出し

監視用ログ出力メソッド内で例外をキャッチし、その例外を再度同じメソッドで記録しようとしたケース:

public class LoggingService {
    public void writeLog(String message) {
        try {
            esClient.index(message); // ES書き込み
        } catch (Exception e) {
            writeLog("ES write failed: " + e.getMessage()); // ⚠️ 再帰呼び出し!
        }
    }
}

このコードはスタック枯渇を引き起こすだけでなく、エラー状態の継続的増幅にもつながります。

修正案:

  • 専用のフェイルセーフロガー(例:ConcurrentLinkedQueue + 単一スレッドフラッシュ)を導入
  • 再帰呼び出しを禁止し、代替出力先(例:標準エラー、ローカルファイル)へフォールバック

TomcatスレッドとHTTPバッファのメモリ負荷

Tomcatのmax-http-header-sizeが10MBに設定されており、RPC依存先のタイムアウトが4秒の場合、QPS 100の負荷で最大400スレッドが同時にアクティブになり得ます。各スレッドが2つの10MBバッファ(リクエスト/レスポンス)を確保すると、合計8GBのヒープ使用量となります。

最適化ポイント:

  • max-http-header-sizeを1MB以下に見直し(実際のヘッダサイズに合わせる)
  • RPCタイムアウトを1–2秒に短縮し、スレッド滞留時間を抑制
  • TomcatのmaxThreadsとJVMヒープサイズのバランス調整

自動診断と監視戦略

JVM起動パラメータの推奨構成

-Xms4g -Xmx4g
-Xmn2g
-Xss512k
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/jvm-dumps/
-XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:+UseG1GC -XX:MaxGCPauseMillis=200

OOM時の自動ダンプ分析フロー

  1. HeapDumpOnOutOfMemoryErrorで.hprofファイルを生成
  2. VisualVMまたはEclipse MATで開き、Histogramビューでトップクラスを確認
  3. Dominator Treeで保持関係をトレースし、ルート参照(GC Roots)を特定
  4. 必要に応じてThread Overviewで問題スレッドのスタックトレースを抽出

運用監視の必須指標

カテゴリ監視項目アラート閾値例
CPU平均ロード(15分)> 3.5(4コア機)
メモリヒープ使用率(CMS/G1 GC後)> 90% 持続5分
GCFull GC頻度> 3回/分
ディスク/var/log 使用率> 95%
アプリケーションリクエスト遅延P99> 2000ms

タグ: JVM OutOfMemoryError Javaメモリ管理 Metaspace GCチューニング

10月18日 16:30 投稿