タイトル: メモリ不足
SEOタイトル: Java のメモリ不足 (OutOfMemoryError) の原因と対処
| この記事の要点 |
|---|
|
OutOfMemoryError の種類
| エラーメッセージ | 領域 | 対処 |
|---|---|---|
Java heap space | ヒープ(オブジェクト) | -Xmx 増 / リーク調査 |
Metaspace | クラスメタ情報(Java 8+) | -XX:MaxMetaspaceSize 増 |
PermGen space | Java 7 以前の Permanent Generation | -XX:MaxPermSize 増(Java 8+ では使わない) |
GC overhead limit exceeded | GC が 98% の時間を使ってヒープの 2% も回収できない | ヒープ増 / リーク調査 |
Unable to create new native thread | OS のスレッド数上限 | 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+ では PermGen が Metaspace に置き換わりました。クラスローダがクラスをアンロードできない場合に発生:
# 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
メモリリーク調査の手順
- ヒープダンプ取得:
jmap -dump:format=b,file=heap.hprof - Eclipse MAT で開く: Eclipse Memory Analyzer
- Leak Suspects レポート生成: 怪しいオブジェクトのリストアップ
- Dominator Tree 確認: 一番ヒープを占有しているオブジェクト
- GC Root から追跡: なぜ GC されないかの参照経路
- コード修正:
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 されない