Java「Out Of Memory Error: Java heap space」の原因と直し方
OutOfMemoryError: Java heap space は、ヒープダンプで設定不足かリークかを見分け、不足なら -Xmx を上げれば直る。
リークなら -Xmx を上げても再発する。
公開: 更新:
java -Xmx64m Leak Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
2026-10-15 に eclipse-temurin:21-jdk(Java 21.0.12.1) で実際に打って取った出力。検証環境の詳細
要約
java.lang.OutOfMemoryError: Java heap space は、ガベージコレクションを走らせてもオブジェクトを置く場所を作れなかった、という報告である。
原因は「ヒープが足りない」か「不要になったはずのオブジェクトが参照されたままで回収できない」かのどちらかだ。
前者なら -Xmx を上げれば済むが、後者はいくら上げても到達時間が延びるだけで再発する。
まず切り分けること。
切り分けの道具はヒープダンプである。
実行例
static な List に 1MB ずつ積み続けるコードを -Xmx64m で動かすと OutOfMemoryError で終了コード 1 になり、-Xmx256m に上げても 96 MB まで確保したところで同じように落ちている。-XX:+HeapDumpOnOutOfMemoryError を付けた実行では java.lang.OutOfMemoryError: Java heap space の直後に約 36MB の heap.hprof が書き出され、落ちた瞬間の中身を後から調べられる状態になった。
$ java -XX:+PrintFlagsFinal -version | grep -i maxheapsize
openjdk version "21.0.12.1" 2026-08-18 LTS
OpenJDK Runtime Environment Temurin-21.0.12.1+1 (build 21.0.12.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.12.1+1 (build 21.0.12.1+1-LTS, mixed mode, sharing)
size_t MaxHeapSize = 4160749568 {product} {ergonomic}
size_t SoftMaxHeapSize = 4160749568 {manageable} {ergonomic}$ java -Xmx64m Leak
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
$ echo $?
1$ java -Xmx256m Leak
確保済み: 32 MB
確保済み: 64 MB
確保済み: 96 MB
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
$ echo $?
1$ java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap.hprof Leak
java.lang.OutOfMemoryError: Java heap space
Dumping heap to heap.hprof ...
Heap dump file created [35962066 bytes in 0.014 secs]
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"
$ echo $?
1$ ls -la heap.hprof
-rw------- 1 root root 35962066 Oct 15 01:16 heap.hprof— 2026-10-15 時点の出力
検証環境Java 21.0.12.1 / 2026-10-15 検証
- 検証日
- 実行環境
eclipse-temurin:21-jdkUbuntu 26.04.1 LTS- バージョン
- Java 21.0.12.1
この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。
解決策
1. 最大ヒープを明示する
まずは現状の上限を確かめ、必要なら明示的に指定する。
# 現在の既定値を確認する
java -XX:+PrintFlagsFinal -version | grep -i maxheapsize
# 最大ヒープを 2GB にして起動する
java -Xmx2g -jar app.jar-Xmx はあくまで上限である。
上げれば落ちなくなるのではなく、落ちるまでの時間が延びる、という点は意識しておくこと。
2. コンテナでは割合で指定する
コンテナでは絶対値よりも割合指定のほうが安全だ。
コンテナのメモリ制限を変えたときに、JVM 側の設定を直し忘れずに済む。
ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0"コンテナのメモリ制限そのものを超えると、JVM ではなくコンテナランタイムに強制終了される。
その場合はヒープの外(メタスペース・スレッドスタック・ネイティブメモリ)も含めた合計が制限を超えていないかを見る。
3. ヒープダンプを取って中身を見る
設定不足かリークかを決めるには、落ちた瞬間の中身を見るのが確実だ。
java -Xmx2g \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heap.hprof \
-jar app.jar出力された .hprof を Eclipse Memory Analyzer などで開き、占有量の大きいオブジェクトと、それを保持している参照の連鎖をたどる。
自分のコードの static フィールドが根元にいたら、それはリークである。
4. 一度に載せる量を減らす
読み込み方の問題であれば、設定ではなく実装を変えるのが本筋だ。
// NG: 全行をメモリに載せる
List<String> lines = Files.readAllLines(path);
// OK: 1 行ずつ処理して捨てる
try (Stream<String> lines = Files.lines(path)) {
lines.forEach(this::process);
}データベースアクセスも同じで、全件取得をやめてページングやカーソルに変えると、件数が増えても消費量が一定に保たれる。
よくある原因
-Xmxを指定していない、あるいは扱うデータ量に対して小さい。
JVM は指定が無ければ物理メモリから既定値を決めるため、開発機では通って本番で落ちる、という差が生まれる。- コンテナで動かしている。
JVM はコンテナのメモリ制限を見て既定値を決めるが、制限の設定漏れやホスト側の余裕との食い違いで、想定より小さいヒープになることがある。 staticなマップやリストに追加し続けている。
自前のキャッシュがサイズ上限を持たない場合の定番だ。- 全件をメモリに載せている。
大きな CSV を丸ごと読む、LIMITの無いクエリ結果をListに詰める、といった実装は件数の増加でそのまま破綻する。