1.

Java のメモリ不足 (OutOfMemoryError) の原因と対処

編集
この記事の要点
  • Java のメモリ不足は主に OutOfMemoryError として表面化
  • Java heap space: -Xmx を増やす(ヒープ領域)
  • Metaspace: -XX:MaxMetaspaceSize を増やす(クラスメタ情報)
  • GC overhead limit exceeded: GC で時間を浪費 → ヒープ増 or リーク調査
  • リーク疑いはヒープダンプを取って Eclipse MAT 等で分析

 

OutOfMemoryError の種類

エラーメッセージ領域対処
Java heap spaceヒープ(オブジェクト)-Xmx 増 / リーク調査
Metaspaceクラスメタ情報(Java 8+)-XX:MaxMetaspaceSize
PermGen spaceJava 7 以前の Permanent Generation-XX:MaxPermSize 増(Java 8+ では使わない)
GC overhead limit exceededGC が 98% の時間を使ってヒープの 2% も回収できないヒープ増 / リーク調査
Unable to create new native threadOS のスレッド数上限ulimit / スレッド数見直し
Direct buffer memoryダイレクトバッファ (NIO)-XX:MaxDirectMemorySize
Requested array size exceeds VM limit配列サイズが JVM 上限超過処理を分割

Java heap space の対処

① ヒープサイズを増やす

# 起動時の JVM オプション
java -Xms512m -Xmx2g -jar myapp.jar

# 単位
# -Xmx2g  = 2 ギガバイト
# -Xmx2048m = 2048 メガバイト
# -Xms = 初期サイズ、-Xmx = 最大サイズ
# 推奨: -Xms と -Xmx は同じ値(動的拡張のオーバーヘッドを避ける)

# Tomcat の場合 (catalina.sh / setenv.sh)
export CATALINA_OPTS="-Xms1g -Xmx2g"

# Spring Boot の場合
java -Xmx2g -jar app.jar
# または
JAVA_OPTS="-Xmx2g" ./gradlew bootRun

② 現在のヒープサイズを確認

# 起動済みプロセスのヒープ情報
$ jcmd  VM.flags | grep -i heap
$ jstat -gccapacity 
$ jmap -heap 

# 例:
$ jmap -heap 1234
Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 2147483648 (2048.0MB)
   NewSize          = 348651520 (332.5MB)
   ...

③ メモリリークの調査

ヒープを増やしても再発するなら、コード側にリークがあります。

# 1. OOM 発生時に自動でダンプを取る設定
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/var/log/heapdump.hprof \
     -jar app.jar

# 2. 手動でダンプ取得
$ jmap -dump:format=b,file=heap.hprof 

# 3. 解析ツール
# - Eclipse MAT (Memory Analyzer Tool) ★最強
# - VisualVM (JDK 同梱)
# - jhat (CLI)

Metaspace OOM の対処

Java 8+ では PermGenMetaspace に置き換わりました。クラスローダがクラスをアンロードできない場合に発生:

# Metaspace 上限を増やす
java -XX:MaxMetaspaceSize=512m -jar app.jar

# 確認
$ jstat -gc 
S0C    S1C    S0U    S1U    EC ...
... MC     MU     CCSC   CCSU
... 80000  78000  10000  9800   ← MC = Metaspace Capacity

頻発する場合:

  • ホットリロード: 開発環境で Spring Boot DevTools 等が大量にクラスを再ロード
  • 動的クラス生成: CGLib / Javassist でプロキシ大量生成
  • クラスローダリーク: webapp のリロード時にクラスローダが GC されない
  • Groovy / Jython: スクリプト言語の動的クラス生成

GC overhead limit exceeded

「GC で 98% の時間を消費しているのにヒープの 2% も回収できない」状態。ヒープがほぼ満杯で、新規オブジェクトを置く隙間がない:

# 一時しのぎ
java -XX:-UseGCOverheadLimit -jar app.jar  # チェックを無効化(非推奨)

# 本質的対処
# - ヒープ増 (-Xmx)
# - メモリリーク調査
# - キャッシュサイズ見直し
# - GC アルゴリズム変更 (G1GC, ZGC)

Tomcat / Spring Boot で OOM

Tomcat (catalina.sh)

# bin/setenv.sh (新規作成)
export CATALINA_OPTS="-server \
    -Xms1g -Xmx4g \
    -XX:MetaspaceSize=128m \
    -XX:MaxMetaspaceSize=512m \
    -XX:+UseG1GC \
    -XX:+HeapDumpOnOutOfMemoryError \
    -XX:HeapDumpPath=/var/log/tomcat-heapdump.hprof \
    -Xloggc:/var/log/tomcat-gc.log \
    -XX:+PrintGCDetails -XX:+PrintGCDateStamps"

Spring Boot (java -jar)

java -server \
  -Xms1g -Xmx4g \
  -XX:+UseG1GC \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/app-heapdump.hprof \
  -jar myapp.jar

メモリリーク調査の手順

  1. ヒープダンプ取得: jmap -dump:format=b,file=heap.hprof
  2. Eclipse MAT で開く: Eclipse Memory Analyzer
  3. Leak Suspects レポート生成: 怪しいオブジェクトのリストアップ
  4. Dominator Tree 確認: 一番ヒープを占有しているオブジェクト
  5. GC Root から追跡: なぜ GC されないかの参照経路
  6. コード修正: ThreadLocal リーク / static フィールド肥大 / リスナー解除漏れ / Connection クローズ忘れ等

よくあるリークパターン

  • 静的 Map / List への追加: static Map cache に追加し続け、削除しない
  • ThreadLocal: set() したが remove() しない
  • イベントリスナー: addListener() したが removeListener() しない
  • JDBC リソース: Connection / Statement / ResultSet を close しない
  • String.intern(): 動的に生成した文字列を intern してメタスペースに蓄積
  • ClassLoader リーク: webapp 再デプロイ時に旧 ClassLoader が GC されない

関連記事

編集
Post Share
子ページ

子ページはありません

同階層のページ

同階層のページはありません