はじめに
本番環境で突然 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が出たらどうする?(この記事)