Java のヒープは余っているのに、なぜかサーバがメモリ不足で落ちる。
原因は JVM の外側 ―― Native Memory と外部プロセスなんてことが良くありました。
本記事は、そんな同居構成で「事故らない」ための、私なりの調査と設定の整理です。
免責
※ 本記事はオンプレミス / Windows Server 環境を前提とした一例であり、システム構成・JDKバージョン・負荷特性により最適解は異なります。
※ 本記事は OpenJDK 11 / 17 系を想定しています。
※ 本構成では ZGC / Shenandoah は検証対象外としています。(Windows + 外部プロセス同居 + 運用実績重視のため)
背景と基本方針
Windows Server 上で Java(Spring Boot)アプリケーションと
外部アプリケーション(例:帳票ツール) が同一サーバ上で稼働しており、
物理メモリ(実メモリ)を奪い合う構成 というようなよくありそうなシチュエーションをもとに、
メモリトラブル時の調査や最適設定を探すときの、私なりの手順をアウトプットしておきます。
このような構成では、Javaヒープ(Inside JVM)だけを見ていても不十分であり、
Native Memory(Outside JVM)を含めた実メモリ消費量の可視化と制御が不可欠だよなーっと捉えています。
そのため、
jcmd を用いて JVM の実メモリ使用状況(特に Native Memory)を定点観測し、
OS・外部プロセスを含めたメモリ配分を再設計する
というアプローチを採用することが良くあります。
※ 本記事は「同一サーバ上で JVM と外部プロセスが同居する構成」を前提としており、コンテナ環境や JVM 単独構成では最適解が異なる場合があります。
超参考情報
こちらは公式情報を見る前に、一読しておくべきと思いました。
こちらは公式情報をざっくり見た後に、目を通すことで何をどうしようか?という疑問を解決してくれると思いました。
ざっくり整理すると
JVM 全体の中での位置づけ
ざっくり階層で見ると・・・
JVM Native Memory
├─ Java Heap ← アプリのオブジェクト
├─ Metaspace ← クラス定義
├─ Code Cache ← JITコード
├─ Thread ← スタック
├─ Internal ← JVM内部管理用
└─ Symbol ← 名前・識別子テーブル
ざっくり図にしてみると・・・
1. jcmd によるメモリ使用量の定期取得
Windows 環境において jcmd を定期実行するには、
PowerShell スクリプトを作成し、タスクスケジューラで定期実行する方式が楽ちんかなーって思っています。
Native Memory Tracking (NMT) の有効化
JVM 起動時に以下のオプションを指定し、再起動する。
-XX:NativeMemoryTracking=summary
※ 詳細調査が必要な短期間のみ detail を使用する感じですかね・・・
(detail は CPU / メモリ負荷が高く、常時有効化は非推奨)
ログ取得スクリプト例(PowerShell)
※ 複数 Java プロセスが存在する可能性を考慮し、単純な Get-Process java の使用は避けることが望ましい
$Date = Get-Date -Format "yyyyMMdd_HHmmss"
# jcmd の一覧から対象 JVM を特定(Spring Boot 等の識別子で絞り込み)
$Pid = (jcmd | Select-String "spring").ToString().Split()[0]
$LogFile = "C:\logs\jcmd_memory_$Date.log"
# Native Memory のサマリ出力
jcmd $Pid VM.native_memory summary > $LogFile
# クラスヒストグラム(クラス増加傾向確認用)
jcmd $Pid GC.class_histogram >> $LogFile
※ PID の誤取得を防ぐため、
可能であれば -Dspring.pid.file=xxx.pid による PID ファイル出力を併用する。
2. 分析のポイント:どこを見るべきか
jcmd VM.native_memory summary の結果から、以下を重点的に確認する。
Reserved vs Committed
- Committed(実使用量)を主指標とする
- Windows では Reserved が大きくても即問題とはならない
- Committed の合計が物理メモリ+ページファイルの上限に近づいていないかを確認する
用語メモ
-
Reserved:「席だけ確保した(使うかもしれないと言って場所を押さえた)」
-
Committed:「実際に人が座っている(本当に使っている)」
障害になるのはほぼ常に Committed かなーって感じています。
Internal / Symbol
-
異常な増加がある場合、以下を疑う
- クラスローダリーク
- リフレクション・動的プロキシの多用
- Spring Boot Fat Jar 特有のクラスロード挙動
- PDFライブラリのクラス解放漏れ
※ 一定水準で頭打ちになっている場合は問題にならないことも多く、「増え続けているかどうか」を時系列で確認することが重要
用語メモ
- Internal:JVM の内部実装が使う Native メモリ
- Symbol: クラス名・メソッド名・フィールド名などの「名前情報(Symbol Table)」
Thread
- 1 スレッドあたり 約 1MB の Native Stack を消費
- Tomcat / Spring Boot のスレッド上限設定と突き合わせる
- スレッド数増加=ヒープ外メモリ増加、という認識を持つ
※ デフォルトでは約1MBだが、-Xss により変更可能
3. GC 設定(CATALINA_OPTS 等)の妥当性確認
最大ヒープサイズ (-Xmx) の見直し
「ヒープを潤沢に割り当てている」構成は、
外部PDF生成プロセスを含む Windows 環境では逆効果になるケースが多い。
例:
- 物理メモリ:32GB
- Java に 28GB 割り当て
→ OS・Native・外部プロセス領域が枯渇しやすい
考え方
Java Xmx = 実メモリ −(OS + PDF生成ピーク + JVM Native 余白)
G1GC の利用
- 同居構成・安定性重視の場合は G1GC を第一候補とする
- メモリ断片化耐性、返却挙動の安定性を重視
-XX:+UseG1GC
Metaspace 上限の設定
Metaspace は Native 領域を使用するため、
上限未設定の場合、Native Memory を際限なく消費するリスクがある。
-XX:MaxMetaspaceSize=512m ~ 1g
※ 小さくしすぎると起動失敗の可能性があるため注意
4. 解決に向けたアプローチ手順
① 現状把握(約 1 週間)
- NMT(summary)有効化
- jcmd による定期ログ取得
- 障害発生直前の Committed 合計値を把握
② 外部プロセス(PDF生成)の負荷測定
-
Windows パフォーマンスモニターを使用
- 対象 PDF プロセスの Working Set / Commit
-
同時起動数とピークメモリ使用量を必ず確認
③ メモリ配分の再設計
- Java Heap:必要最小限まで削減
- JVM Native + OS + PDF生成用の余白を確保
- 同時 PDF 生成数が多い場合は制御(キュー化)を検討
④ 設定反映と再検証
- CATALINA_OPTS 反映
- 再度 jcmd / PerfMon を用いて挙動を確認
- Committed が安定して頭打ちになるかを評価
注意点(Windows Server 特有?)
- タスクマネージャーの「利用可能」メモリは信用しない
- **「コミット済み(Committed Bytes)」と「Commit Limit」**を必ず確認
- ページファイルが小さい構成は即座にメモリ枯渇を招く
まとめ
まずは、
Native Memory Tracking (summary) を有効化し、jcmd による数日〜1週間のログ収集を行う
ことから着手する。
これにより、
- Java が原因なのか
- 外部 PDF プロセスが主因なのか
- OS のメモリ設計が破綻しているのか
を 定量的に切り分けることが可能となるかなーと思います。
最後に
Java のメモリトラブルは、「ヒープを見て終わり」では解決しないケースが存在すると思っています。
特に JVM と外部プロセスが同居する構成では、Native Memory・OS・GC の振る舞い を1つのシステムとして捉える視点が重要だと感じています。
同じような構成で悩んでいる方の、調査・設計のヒントになれば幸いです。
以下は、よく忘れがちなので手書きノートから転記しておきます。
GC 実行時オプション設定~判断材料のエッセンス整理~
前提条件(この判断が必要な環境)
- Windows Server
- Java(Spring Boot)+ 外部 PDF 生成プロセスが同居
- CPU / メモリを JVM が独占してはいけない
1. なぜ GC の実行時オプションを「明示指定」するのか
JVM デフォルトの問題点
-
JVM は CPU コア数 ≒ GC スレッド数 で自動設定する
-
その結果:
- GC 実行時に CPU を一気に使い切る
- 外部 PDF プロセスが詰まる
- Java / PDF 両方が不安定になる
デフォルト任せは「単独 JVM 前提」
同居構成では危険
2. 最低限、制御すべき GC パラメータ
必須で見るのはこの 2 つかなーー
-XX:ParallelGCThreads
-XX:ConcGCThreads
| パラメータ | 意味 | 目的 |
|---|---|---|
| ParallelGCThreads | STW GC の並列数 | CPU の瞬間独占を防ぐ |
| ConcGCThreads | 並行 GC の並列数 | 平常時の CPU 食いを防ぐ |
「GC に CPU を全部渡さない」ための制御
3. 採用すべき GC 戦略の判断
基本方針
- 同居構成・安定性重視の場合は G1GC を第一候補とする
- Parallel GC は第二候補
理由:
- 停止時間が分散される
- 外部プロセス同居に向く
- チューニング余地がある
4. GC スレッド数の決め方(判断ルール)
原則ルール(暗記用)
GC スレッド数は「CPU の 1/3〜1/4 程度」
具体例
| vCPU | ParallelGCThreads | ConcGCThreads |
|---|---|---|
| 8 | 2〜3 | 1 |
| 16 | 4〜5 | 2 |
| 32 | 6〜8 | 3 |
CPU 全部を GC に使わせないことが最重要
5. GC 停止時間に対する考え方
-XX:MaxGCPauseMillis
- 「目標値」であって保証ではない
- 値を小さくしすぎると GC が暴れる
実務目安
500ms 前後
安定性優先、低遅延は二の次
6. 実行時オプション決定の判断フロー(超重要)
私的にはこんな判断をしている。
7. エッセンスだけを抜いた最小構成例
-XX:+UseG1GC
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
-XX:MaxGCPauseMillis=500
これだけで「事故りにくさ」が段違い というイメージです。
8. 判断の軸(判断責任者へのレビュー用ひとこと)
「GC の速さより、サーバ全体の安定性を優先する構成」が私としては好みですねーっと言ってみる。