できない.dev

Java で「OutOfMemoryError: Java heap space」が解消できない

ヒープに空きを作れずアロケーションが失敗した状態。-Xmx を上げれば通ることもあるが、参照が残り続けているだけの場合は上げても再発する。
まずヒープダンプで何が残っているかを見て、設定不足かリークかを切り分ける。

公開:

要約

java.lang.OutOfMemoryError: Java heap space は、ガベージコレクションを走らせてもオブジェクトを置く場所を作れなかった、という報告です。

原因は「ヒープが足りない」か「不要になったはずのオブジェクトが参照されたままで回収できない」かのどちらかです。
前者なら -Xmx を上げれば済みますが、後者はいくら上げても到達時間が延びるだけで再発します。

まず切り分けてください。
切り分けの道具はヒープダンプです。

よくある原因

  1. -Xmx を指定していない、あるいは扱うデータ量に対して小さい。
    JVM は指定が無ければ物理メモリから既定値を決めるため、開発機では通って本番で落ちる、という差が生まれます。
  2. コンテナで動かしている。
    JVM はコンテナのメモリ制限を見て既定値を決めますが、制限の設定漏れやホスト側の余裕との食い違いで、想定より小さいヒープになることがあります。
  3. static なマップやリストに追加し続けている。
    自前のキャッシュがサイズ上限を持たない場合の定番です。
  4. 全件をメモリに載せている。
    大きな CSV を丸ごと読む、LIMIT の無いクエリ結果を List に詰める、といった実装は件数の増加でそのまま破綻します。

解決策

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);
}

データベースアクセスも同じで、全件取得をやめてページングやカーソルに変えると、件数が増えても消費量が一定に保たれます。

この記事は役立ちましたか?