1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Java】OutOfMemoryErrorが出たらどうする? ― 焦らず原因を切り分けるための初動ガイド

1
Posted at

はじめに

本番環境で突然 OutOfMemoryError(以下OOM)が発生した ―― Java開発をしていると、いつか必ず出会うエラーです。

前回までの記事で「ヒープとスタック」「GC」を学びました。今回はその知識を使って、OOMが出たときに焦らず原因を切り分ける方法を解説します。

対象読者: Java実務1〜3年目。OOMに遭遇したことがある(またはこれから遭遇しそうな)エンジニア


1. OOMは1種類じゃない

OutOfMemoryError と一口に言っても、メッセージによって原因がまったく違います。まずはメッセージの種類を知ることが第一歩です。

エラーメッセージ 溢れた場所 よくある原因
Java heap space ヒープ(オブジェクト領域) オブジェクトの作りすぎ、メモリリーク
Metaspace メタスペース(クラス情報領域) クラスの動的生成が多すぎる
GC overhead limit exceeded ヒープ GCしても全然メモリが空かない状態
unable to create new native thread OS側のリソース スレッドの作りすぎ

実務で最もよく見るのは Java heap space です。この記事ではこれを中心に解説します。


2. 典型的な原因パターン

パターン①: 単純にヒープが足りない

// 大量データを一括でリストに載せてしまう
public List<Record> loadAllRecords() {
    return jdbc.query("SELECT * FROM huge_table"); // 100万件がListに乗る
    // → ヒープが足りずOOM
}

対処: ページネーションやストリーム処理に変更する

// 改善例: ページ単位で処理
public void processAllRecords() {
    int offset = 0;
    int pageSize = 1000;
    List<Record> page;
    do {
        page = jdbc.query(
            "SELECT * FROM huge_table LIMIT ? OFFSET ?", pageSize, offset
        );
        page.forEach(this::process);
        offset += pageSize;
    } while (!page.isEmpty());
}

パターン②: メモリリーク

public class LeakExample {
    // staticなコレクションに追加し続ける → GCで回収されない
    private static List<byte[]> leakyList = new ArrayList<>();

    public void doWork() {
        byte[] data = new byte[1024 * 1024]; // 1MB
        leakyList.add(data);  // 参照が残り続けるのでGCが回収できない
    }
}

GCは「参照されなくなったオブジェクト」しか回収できません。static フィールドのコレクションにオブジェクトを追加し続けると、参照が切れないのでヒープが減らず、いずれOOMになります。

パターン③: GC overhead limit exceeded

// ヒープのほとんどが埋まった状態でGCが繰り返される
// → GCに全体時間の98%以上を費やしてもヒープの2%未満しか回収できない
// → JVMが「もうダメだ」と判断してこのエラーを出す

これは「ヒープがギリギリで、GCが必死に動いているけど追いつかない」という状態です。根本的にはメモリリークか、ヒープサイズ不足のどちらかです。


3. OOMが出たときの初動チェックリスト

ステップ1: エラーメッセージを確認する

java.lang.OutOfMemoryError: Java heap space
                            ^^^^^^^^^^^^^^^^
                            ここを見る!

メッセージの種類で、どの領域が溢れたのかがわかります(セクション1の表を参照)。

ステップ2: 現在のヒープ設定を確認する

# 実行中のJavaプロセスのPIDを調べる
jps

# ヒープ設定を確認
jinfo -flags <PID>

よく見るオプション:

オプション 意味
-Xms ヒープの初期サイズ -Xms512m
-Xmx ヒープの最大サイズ -Xmx2g
-XX:MetaspaceSize メタスペースの初期サイズ -XX:MetaspaceSize=256m

-Xmx が小さすぎないか?」をまず確認しましょう。デフォルト値はJVMやOS環境によって異なりますが、意外と小さいことがあります。

ステップ3: ヒープダンプを取得する

# 事前に設定しておく(OOM発生時に自動でダンプを出力)
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/tmp/heapdump.hprof \
     -jar myapp.jar
# 実行中のプロセスから手動で取得
jmap -dump:format=b,file=/tmp/heapdump.hprof <PID>

ヒープダンプは「その瞬間のヒープの中身のスナップショット」です。これを分析ツールで開くと、何のオブジェクトがメモリを食っているのかが見えます。

ステップ4: ヒープダンプを分析する

分析には Eclipse Memory Analyzer(MAT) が定番です(無料)。

MATで見るポイント:
1. Leak Suspects Report → 怪しいオブジェクトを自動検出
2. Top Consumers → メモリを多く使っているオブジェクトのランキング
3. Dominator Tree → 「このオブジェクトが消えたらこれだけ空く」を表示

例えば「HashMap が500MBも食っている」とわかれば、そのHashMapを持っているクラスを辿って原因を特定できます。


4. よくある「やりがちミス」と対策

❌ とりあえず -Xmx を増やす

# 場当たり的な対処(根本解決ではない)
java -Xmx8g -jar myapp.jar

ヒープを増やせば一時的にOOMは避けられますが、メモリリークが原因の場合はいずれまた溢れます。しかも、ヒープが大きいとFull GCの停止時間も長くなるおまけ付きです。

✅ まずは原因を特定してから対処する

OOM発生
  ├→ エラーメッセージ確認
  ├→ -Xmx設定確認(そもそも適切か?)
  ├→ ヒープダンプ取得・分析
  └→ 原因に応じた対処
       ├ メモリリーク → コード修正
       ├ データ量超過 → 処理方式の見直し
       └ 設定不足 → ヒープサイズ調整(最後の手段)

まとめ

  • OOMはメッセージの種類を見て原因の領域を切り分ける
  • 最も多いのは Java heap space(オブジェクトの作りすぎ or メモリリーク)
  • 初動は「メッセージ確認 → ヒープ設定確認 → ヒープダンプ取得」の3ステップ
  • -Xmx を増やすのは場当たり的。まず原因を特定するのが大事
  • -XX:+HeapDumpOnOutOfMemoryError を事前に設定しておくと、いざというとき助かる

シリーズ記事

  • 第1回: ヒープとスタック
  • 第2回: GC(ガベージコレクション)の基本
  • 第3回: OutOfMemoryErrorが出たらどうする?(この記事)
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?