はじめに
Java の JVM options は数が多く、全部を覚えるのは現実的ではありません。
一方で、Spring Boot などの Java Web Server を Kubernetes 上で動かす場合、いくつかの option はリソース設計や障害調査にかなり直接効いてきます。
この記事では、Kubernetes 上で Java Web Server を動かす前提で、
- どの JVM option が何に効くのか
- default 値はどうなっているのか
- 設定値のあたりをどう付けるか
- Kubernetes の
requests/limitsとどう関係するか
を整理します。
なお、この記事は筆者の本番運用結果をまとめたものではなく、公式ドキュメントを中心に調べたメモです。実際の設定値はアプリケーションの特性、JDK のバージョン、利用しているフレームワーク、Pod のリソース設定によって変わります。
先に一覧
まず、Kubernetes 上の Java Web Server で見ておきたい JVM options をざっくり並べます。
| option | default | 効くところ | まずの考え方 |
|---|---|---|---|
-XX:+UseContainerSupport |
有効 | cgroup のメモリ/CPU認識 | 最近の JDK では基本そのまま |
-XX:MaxRAMPercentage |
25 |
最大ヒープ | Web Server では 60 から 75 くらいを候補にする |
-XX:InitialRAMPercentage |
1.5625 |
初期ヒープ | 起動直後の負荷が重いなら上げる |
-Xmx |
実行環境から自動決定 | 最大ヒープ | 固定したい場合に使う |
-Xms |
実行環境から自動決定 | 初期ヒープ | ヒープ拡張の揺れを減らしたい場合に使う |
-XX:ActiveProcessorCount |
自動検出 | JVM が見る CPU 数 | CPU limit 下で明示したい場合に使う |
-XX:+UseG1GC |
有効 | GC | Java 17/21 では基本線 |
-XX:MaxGCPauseMillis |
200 |
G1 の pause time 目標 | latency 重視なら下げる候補 |
-XX:+UseZGC |
無効 | GC | 低レイテンシ重視なら候補 |
-XX:SoftMaxHeapSize |
未指定 | ZGC の軟上限 | ZGC 利用時に通常時のヒープ使用量を抑えたい場合 |
-XX:MaxDirectMemorySize |
自動 | Direct Buffer | Netty などを使う場合に見る |
-Xss |
platform dependent | thread stack | スレッド数が多い場合に効く |
-XX:ReservedCodeCacheSize |
JDK により異なる | JIT code cache | 通常は触らず、警告が出たら見る |
-XX:NativeMemoryTracking=summary |
off |
native memory 調査 | 調査時に有効化する |
-XX:+HeapDumpOnOutOfMemoryError |
無効 | OOME 時の heap dump | 保存先と容量を考えて入れる |
-XX:HeapDumpPath |
current directory | heap dump 出力先 | Kubernetes では volume mount とセット |
-Xlog:gc* |
未指定 | GC ログ | まず入れて観測したい |
-Xlog:os+container=info |
未指定 | container 認識ログ | JVM が cgroup をどう見たか確認する |
この表だけ見るとたくさんありますが、常に全部を指定するという意味ではありません。
最初に見るべきなのは、だいたい次の 4 つです。
-XX:MaxRAMPercentage
-Xlog:gc*
-Xlog:os+container=info
-XX:+HeapDumpOnOutOfMemoryError
必要に応じて、CPU や GC、native memory の option を足していくのがよさそうです。
Kubernetes では JVM の外側にもメモリがある
JVM のメモリ設定を考えるとき、まず大事なのは Kubernetes の memory limit は Java heap だけではなく、コンテナ内のプロセス全体に効く という点です。
Java プロセスのメモリには、ざっくり次のような領域があります。
- Java heap
- Metaspace
- Direct Buffer
- Thread Stack
- Code Cache
- GC や JIT が使う native 領域
- JNI や native library が使う領域
- monitoring agent などが使う領域
そのため、Pod の memory limit が 1Gi のときに -Xmx1g のように設定すると、heap 以外の領域の余裕がほとんどなくなります。
Kubernetes の memory limit は Linux の cgroup によって制御され、超過するとプロセスが OOM kill される可能性があります。これは Java の OutOfMemoryError とは別の話です。
heap 以外を先に確保して、残りを heap に渡す
一番悩ましいのが、heap 以外のメモリをどのくらい残せばよいかです。
ここは、heap サイズを先に決めて残りを heap 外にする よりも、heap 外に必要なメモリを先に見積もって、残りを heap に渡す と考える方が自然です。
理由は、heap 外メモリには比較的固定費に近いものが多いからです。
最大ヒープ = container memory limit - heap 外メモリの見積もり - 安全マージン
例えば memory limit が 1Gi の Pod で、heap 外に 250Mi、安全マージンに 50Mi 残したいとします。
container memory limit: 1024Mi
heap 外の見積もり: 250Mi
安全マージン: 50Mi
最大ヒープ: 724Mi
この場合、最大ヒープはおおよそ 724Mi です。MaxRAMPercentage で表現するなら、だいたい 70 になります。
724 / 1024 * 100 = 約70.7%
つまり、MaxRAMPercentage=70 という値を先に決めるというより、heap 外に必要な量を見積もった結果として 70 前後になる、という順番です。
ざっくりした初期値としては、普通の Spring Boot Web API なら memory limit の 25% から 35% 程度を heap 外 + 安全マージンとして残す ところから考えるのがわかりやすそうです。
一方で、次のようなアプリケーションでは heap 外を多めに見た方が安全です。
| 状況 | heap 外を多めに見る理由 | 考え方 |
|---|---|---|
| Netty / gRPC / WebFlux を使う | Direct Buffer が増えやすい | direct buffer 分を先に残す |
| thread 数が多い | thread stack が増える | thread数 * Xss を見る |
| monitoring agent を入れる | agent 自体が native/heap を使う | agent 分の余白を残す |
| JNI / native library を使う | NMT で見えない native memory もあり得る | 安全マージンを厚めにする |
| 小さい Pod で動かす | 固定費の比率が大きくなる | heap 割合を上げすぎない |
逆に、heap 使用量が支配的で、thread 数も direct memory も少なく、RSS に十分余裕があることを確認できているなら、75 前後まで寄せる余地があります。
ただし、MaxRAMPercentage=80 以上のように heap に大きく寄せる設定は、heap 外の余白がかなり小さくなります。Kubernetes では少し超えただけでも OOMKilled になり得るため、最初の値としては攻めすぎに見えます。
MaxRAMPercentage は便利ですが、「heap 外にどれだけ残すか」を直接指定する option ではありません。そのため、設計の順番としては次の方がわかりやすいです。
1. memory limit を決める
2. heap 外メモリを見積もる
3. 安全マージンを足す
4. 残りを最大ヒープにする
5. その値を Xmx または MaxRAMPercentage で表現する
heap 外メモリの内訳をざっくり見積もる
heap 外メモリは、完全にきれいな式で見積もるのは難しいです。それでも、主な項目ごとに「あたり」を付けることはできます。
Metaspace
Metaspace は class metadata などに使われます。
Spring Boot のように読み込む class が多いアプリケーションでは、それなりに増えます。まずは起動後に次のようなコマンドで確認します。
jcmd <pid> VM.metaspace
または、Native Memory Tracking を有効にしている場合は、Class や Metaspace に相当する項目を見ます。
jcmd <pid> VM.native_memory summary scale=MB
Metaspace は通常、起動直後から warm-up 後にかけて増え、その後は比較的落ち着くことが多いです。継続的に増え続ける場合は、classloader leak や動的 class 生成を疑います。
Thread Stack
thread stack は、だいたい次のように見積もれます。
thread stack の予約量 = thread 数 * Xss
例えば -Xss1m で thread が 200 本あるなら、stack の予約量は単純計算で 200Mi です。
実際に全量が物理メモリとして使われるわけではありませんが、スレッド数が多いアプリケーションでは無視できません。
thread 数は次で見られます。
jcmd <pid> Thread.print
NMT を有効にしていれば、Thread の項目に thread 数や stack の reserved / committed が出ます。
jcmd <pid> VM.native_memory summary scale=MB
大量の platform thread を使う servlet stack、scheduler、非同期処理、blocking I/O が多いアプリケーションでは、thread 数を一度確認しておくとよさそうです。
Direct Buffer
Direct Buffer は heap の外に確保されます。
Netty、gRPC、WebFlux、NIO を多く使うアプリケーションでは、ここが効くことがあります。
関連する上限 option は次です。
-XX:MaxDirectMemorySize=<size>
Direct Buffer の使用量は、アプリケーションやフレームワークの metrics で見えることがあります。Micrometer / Spring Boot Actuator を使っている場合は、JVM buffer metrics を確認します。
代表的には次のような指標です。
jvm.buffer.memory.used
jvm.buffer.total.capacity
jvm.buffer.count
id=direct の値を見ると direct buffer の傾向を把握しやすいです。
Code Cache
Code Cache は JIT compiler が生成した native code を置く領域です。
通常は最初から細かく見積もらなくてよいですが、NMT の Code 項目や次のログで確認できます。
-Xlog:codecache=info
CodeCache full の警告が出ていなければ、最初は default のままでよさそうです。
JVM native / GC / JIT / agent
GC、JIT、JVM 自体、monitoring agent なども heap 外メモリを使います。
ここはアプリケーションから直接見えにくい領域です。NMT を有効にすると、GC、Compiler、Internal、Symbol、Native Memory Tracking などのカテゴリとして見えます。
ただし、Oracle の NMT ドキュメントでも説明されている通り、NMT は HotSpot VM の内部メモリを追跡する機能であり、third-party native code の allocation まですべて追跡できるわけではありません。
そのため、NMT の合計と container の RSS が大きくずれる場合は、JNI、native library、glibc malloc、agent など JVM 外の native allocation も疑います。
足りているかどうかをどうモニターするか
heap 外メモリが足りているかは、1つの metrics だけでは判断しにくいです。
まず見るべきは次の差分です。
container memory usage - Java heap used
この差分が大きい、または時間とともに増え続ける場合、heap 外メモリを見ます。
Kubernetes 側では、まず Pod の memory usage を見ます。
kubectl top pod <pod-name>
より詳しく見るなら、利用している監視基盤で container memory の metrics を見ます。Prometheus / cAdvisor 系なら、例えば次のような指標です。
container_memory_working_set_bytes
container_memory_rss
Java 側では、heap used、heap committed、non-heap、buffer、thread 数を見ます。Micrometer / Spring Boot Actuator を使っているなら、次のような metrics が参考になります。
jvm.memory.used
jvm.memory.committed
jvm.memory.max
jvm.buffer.memory.used
jvm.threads.live
NMT を有効にできる環境なら、調査時に次のように baseline を取ります。
jcmd <pid> VM.native_memory baseline
しばらく負荷をかけた後、差分を見ます。
jcmd <pid> VM.native_memory summary.diff scale=MB
Oracle の NMT ドキュメントでは、reserved と committed が出てきます。実際に物理メモリとして効くのは主に committed 側です。reserved が大きくても、それだけで直ちにメモリ不足とは限りません。
見るポイントは次です。
Java Heap以外のcommittedがどれくらいあるかThreadの thread 数と stack が想定より多くないかClass/Metaspaceが増え続けていないかCodeが上限に近づいていないかGCやInternalが異常に増えていないか- NMT の合計より container RSS がかなり大きくないか
NMT は便利ですが、summary でもオーバーヘッドがあります。Oracle のドキュメントでは、NMT の有効化に 5% から 10% 程度の performance overhead があると説明されています。そのため、常時有効にするというより、検証環境や調査時に使うのが扱いやすそうです。
heap 外メモリが足りないときのサイン
heap 外メモリが足りない場合、Java heap の OOM とは違う見え方をすることがあります。
代表的には次のようなサインです。
- Pod が
OOMKilledになる - heap used は
Xmxに張り付いていないのに container memory usage が高い OutOfMemoryError: Direct buffer memoryが出るunable to create native threadが出る- Metaspace OOM が出る
- CodeCache full の warning が出る
- RSS が右肩上がりだが heap dump では原因が見えない
このうち、Pod が OOMKilled になっている場合は、JVM が例外や heap dump を出す前に kernel に kill されている可能性があります。
kubectl describe pod <pod-name>
で Last State や Reason: OOMKilled を確認します。
heap dump だけを見て「heap は余っているから問題ない」と判断すると、direct memory や thread stack、native memory の問題を見逃すことがあります。
container 認識: UseContainerSupport
HotSpot JVM には、コンテナの cgroup 情報を見てメモリや CPU 数を判断する仕組みがあります。
-XX:+UseContainerSupport
Oracle の Java command docs では、Linux 上でコンテナ検出を行い、利用可能なメモリ量や CPU 数を判断すると説明されています。最近の JDK では default で有効です。
基本的には明示しなくてもよい option ですが、古い JDK や cgroup v2 の環境では注意が必要です。
特に cgroup v2 については、OpenJDK の対応時期に差があります。目安としては、Java 15 以降、または backport 済みの Java 11.0.16 以降、Java 8u372 以降を使うのが安全です。
JVM がコンテナをどう認識しているかは、次の option で確認できます。
java -XshowSettings:system -version
アプリケーション起動時にログとして残すなら、次のようにします。
-Xlog:os+container=info
より詳しく見る場合は trace も使えます。
-Xlog:os+container=trace
普段の運用ログとしては info、調査時には trace くらいの使い分けがよさそうです。
最大ヒープ: Xmx と MaxRAMPercentage
最大ヒープを決める代表的な option は次の 2 つです。
-Xmx
-XX:MaxRAMPercentage
-Xmx は最大ヒープサイズを直接指定します。
-Xmx768m
一方、-XX:MaxRAMPercentage は、JVM が利用可能と判断したメモリに対して、最大ヒープを何パーセントにするかを指定します。
-XX:MaxRAMPercentage=75
Oracle の Java command docs では、MaxRAMPercentage の default は 25 です。
つまり、コンテナの memory limit が 1Gi で、JVM がその値を正しく認識している場合、default の最大ヒープはおおよそ 256Mi になります。
これは安全側ではありますが、Java Web Server としては少なすぎることもあります。そのため、Web Server 用途では 60 から 75 くらいを候補にすることが多そうです。
例えば memory limit が 1Gi の Pod で次のように設定した場合、
-XX:MaxRAMPercentage=75
最大ヒープはおおよそ 768Mi になります。
残りの約 256Mi を、Metaspace、Direct Buffer、Thread Stack、Code Cache、JVM native 領域などに残す考え方です。
ただし、次のような場合は 75 だと高すぎる可能性があります。
- Netty や gRPC などで Direct Buffer を多く使う
- worker thread 数が多い
- 大きめの monitoring agent を入れている
- JNI や native library を使っている
- metaspace が大きくなりやすい
この場合は 60 から 70 くらいを起点にして、RSS、GCログ、native memory を見ながら調整するのがよさそうです。
なお、-Xmx を指定した場合、最大ヒープは -Xmx が優先されます。Pod の memory limit を変えても自動で追従しないため、Kubernetes のリソース変更と JVM option の変更がずれないように注意が必要です。
初期ヒープ: Xms と InitialRAMPercentage
初期ヒープを決める option は次の 2 つです。
-Xms
-XX:InitialRAMPercentage
-Xms は初期ヒープサイズを直接指定します。
-Xms768m
-XX:InitialRAMPercentage は、JVM が利用可能と判断したメモリに対して、初期ヒープを何パーセントにするかを指定します。
-XX:InitialRAMPercentage=50
Oracle の Java command docs では、InitialRAMPercentage の default は 1.5625 です。最大ヒープに比べるとかなり小さい値です。
Web Server の場合、起動直後からある程度のアクセスを受けることがあります。初期ヒープが小さいと、起動後しばらくヒープ拡張や GC の挙動が揺れやすくなる可能性があります。
設定方針としては、ざっくり次のように考えられます。
- メモリ使用量を抑えたいなら default に近い値から始める
- 起動後の性能を安定させたいなら
InitialRAMPercentageを上げる - ヒープ拡張の揺れを減らしたいなら
XmsとXmxを同じにする
ただし、Xms = Xmx は起動時からヒープを大きく確保する方向の設定です。Pod 密度を高めたい環境や、アイドル時間が長いアプリケーションではメモリ効率が悪くなる可能性があります。
Kubernetes では、常に最大負荷を想定してメモリを確保したいのか、それともアイドル時の使用量も抑えたいのかで判断が分かれそうです。
小さいヒープ: MinRAMPercentage
-XX:MinRAMPercentage は名前だけ見ると最小ヒープの割合に見えますが、Oracle の docs では「small heap に対する最大ヒープの割合」と説明されています。
default は 50 です。
small heap はおおよそ 125MB 程度と説明されており、かなり小さいコンテナで Java を動かす場合に効いてきます。
通常の Web Server 用途では MaxRAMPercentage をまず見ればよいですが、非常に小さい memory limit の Pod では、MinRAMPercentage の影響も確認した方がよさそうです。
実際の値は次のように確認できます。
java -XX:+PrintFlagsFinal -version | grep RAMPercentage
CPU: ActiveProcessorCount
CPU 周りで見ておきたい option がこれです。
-XX:ActiveProcessorCount=<number>
これは JVM が利用可能とみなす CPU 数を上書きします。
JVM は CPU 数をもとに、GC thread 数や ForkJoinPool の parallelism などを決めます。Kubernetes では CPU request と CPU limit があり、特に CPU limit は throttling として効きます。
Kubernetes の公式ドキュメントでは、CPU limit は kernel によって強制され、上限に近づくと throttling されると説明されています。
例えば、Pod に次のような設定をしているとします。
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
この場合、CPU は最大 1 core 相当までに制限されます。
JVM が CPU 数をどう認識しているかは、次で確認できます。
java -XshowSettings:system -version
もし実際の CPU 制限と JVM の見積もりを明示的に揃えたい場合は、次のように指定できます。
-XX:ActiveProcessorCount=1
ただし、これも常に指定すべき option ではありません。まずは JVM がコンテナの CPU をどう見ているか確認し、GCログや latency、CPU throttling の状況を見てから検討するのがよさそうです。
GC: まずは G1 を基本にする
Java 17 や Java 21 の HotSpot では、G1 GC が default です。
-XX:+UseG1GC
Oracle の G1 GC tuning guide でも、まずは G1 の default 設定を使い、必要に応じて pause time goal や最大ヒープサイズを設定する、という方針が示されています。
G1 でよく見る option はこれです。
-XX:MaxGCPauseMillis=200
default は 200 ms です。
ただし、これは「最大 pause time を保証する値」ではなく、G1 が目指す目標値です。小さくすれば必ず latency が改善するわけではなく、GC 頻度が増えたり、throughput が落ちたりする可能性もあります。
考え方としては、次のようになります。
- まず default の G1 で GC ログを見る
- latency が問題なら
MaxGCPauseMillis=100などを試す - throughput が重要なら無理に下げない
- Full GC や evacuation failure が出ているなら、pause target だけでなくヒープサイズや allocation rate も見る
GCログは最低限、次のように出しておくと調査しやすくなります。
-Xlog:gc*:stdout:time,level,tags
ファイルに出す場合は、ログローテーションも含めて考えます。
-Xlog:gc*:file=/logs/gc.log:time,level,tags:filecount=5,filesize=20M
Kubernetes では stdout に出してログ基盤に流す方が扱いやすいことも多いです。
低レイテンシ用途なら ZGC も候補
低レイテンシを重視する場合は、ZGC も候補になります。
-XX:+UseZGC
Oracle の ZGC tuning guide では、ZGC で最も重要な tuning option は最大ヒープサイズ、つまり -Xmx だと説明されています。ZGC は concurrent collector なので、GC が動いている間にもアプリケーションが allocation できるだけの余裕が必要です。
ZGC では次の option もあります。
-XX:SoftMaxHeapSize=<size>
これは「できるだけこのサイズ以下に収めたい」という軟上限です。ZGC は必要であれば -Xmx まで使えますが、通常時のヒープ使用量を抑える目安として SoftMaxHeapSize を使えます。
例えば、
-Xmx1g
-XX:SoftMaxHeapSize=768m
のようにすると、最大では 1Gi まで使えるが、通常時は 768Mi を目標にする、という考え方になります。
ただし、ZGC は低レイテンシ向けの選択肢であり、常に G1 よりよいというものではありません。Web Server の latency 要件、メモリ余裕、throughput 要件を見て選ぶ必要があります。
Direct Memory: MaxDirectMemorySize
Direct Buffer を多く使うアプリケーションでは、heap 以外のメモリ使用量が大きくなります。
関連する option はこれです。
-XX:MaxDirectMemorySize=<size>
Netty、gRPC、Reactive stack などでは Direct Buffer が効いてくることがあります。
ただし、最初から必ず指定するというより、RSS が大きい、heap は余っているのに container が OOMKilled される、native memory が大きい、といった状況で見る option だと思います。
調査時には Native Memory Tracking を使うと、heap 外の内訳を見やすくなります。
-XX:NativeMemoryTracking=summary
起動後に jcmd で確認します。
jcmd <pid> VM.native_memory summary
NMT は調査に便利ですが、オーバーヘッドがあります。常時有効にするかどうかは環境に応じて判断します。
Thread Stack: Xss
スレッドごとに stack memory が確保されます。
関連する option はこれです。
-Xss<size>
Oracle の Java command docs では、default は platform dependent とされています。例として Linux/x64 では 1024 KB、Linux/AArch64 では 2048 KB と説明されています。
例えば -Xss1m で thread が 300 本あると、単純計算で stack だけで最大 300MiB 程度の予約になります。
もちろん、実際の committed memory とは分けて考える必要がありますが、スレッド数が多い Web Server では無視できない領域です。
Xss を小さくするとメモリ使用量を抑えられる可能性がありますが、小さすぎると StackOverflowError のリスクがあります。フレームワーク、再帰処理、filter chain、serialization などの影響も受けるため、慎重に扱う option です。
Code Cache: ReservedCodeCacheSize
JIT compiler が生成した native code は Code Cache に置かれます。
関連する option はこれです。
-XX:ReservedCodeCacheSize=<size>
通常の Web Server では最初から触る必要はあまりなさそうです。
ただし、ログに CodeCache が full になったことを示す警告が出る場合や、非常に多くのコードを動的に生成するアプリケーションでは確認対象になります。
まずは次のようなログで状況を見るのがよさそうです。
-Xlog:codecache=info
OOM 調査: Java OOME と Kubernetes OOMKilled は別物
OOM 周りでよく使う option は次の 2 つです。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dumps
HeapDumpOnOutOfMemoryError は、Java の OutOfMemoryError が発生したときに heap dump を出力します。default は無効です。
Kubernetes 上で使うなら、dump の出力先を明示して、必要に応じて volume を mount します。
volumeMounts:
- name: heap-dumps
mountPath: /dumps
volumes:
- name: heap-dumps
emptyDir: {}
ただし、重要なのは Java の OutOfMemoryError と Kubernetes の OOMKilled は別物 という点です。
Java heap が足りなくなって JVM が OutOfMemoryError を投げた場合は、heap dump が出る可能性があります。
一方で、コンテナ全体が memory limit を超えて kernel に kill された場合、JVM が heap dump を出す前にプロセスが終了することがあります。この場合は kubectl describe pod で OOMKilled を確認する必要があります。
OOM 調査では、最低限次を分けて見ます。
- Java heap が足りないのか
- direct memory や thread stack など heap 外が大きいのか
- container の memory limit が小さすぎるのか
- GC が追いついていないのか
- memory leak なのか、単に負荷に対して heap が足りないのか
Kubernetes での設定例
ここまでを踏まえると、まずの設定例は次のようになります。
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample-java-app
spec:
replicas: 2
selector:
matchLabels:
app: sample-java-app
template:
metadata:
labels:
app: sample-java-app
spec:
containers:
- name: app
image: example.com/sample-java-app:latest
ports:
- containerPort: 8080
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-XX:MaxRAMPercentage=70
-XX:InitialRAMPercentage=30
-Xlog:os+container=info
-Xlog:gc*:stdout:time,level,tags
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dumps
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
volumeMounts:
- name: heap-dumps
mountPath: /dumps
volumes:
- name: heap-dumps
emptyDir: {}
この例では、memory limit 1Gi に対して、heap 外 + 安全マージンとして約 300Mi を残す想定にしています。
その結果、最大ヒープはおおよそ 700Mi くらいになり、MaxRAMPercentage=70 で表現しています。
InitialRAMPercentage=30 は、起動直後からある程度の heap を持たせるための例です。実際には起動時間、アイドル時メモリ、負荷特性を見て調整します。
設定値のあたりの付け方
個人的には、調査ベースで次の順番がわかりやすいと思っています。
1. memory limit を決める
まず Pod の memory limit を決めます。
resources:
requests:
memory: "1Gi"
limits:
memory: "1Gi"
Web Server の場合、memory request と limit を同じにする構成は考えやすいです。Pod の QoS class も Guaranteed に寄せやすくなります。
ただし、CPU まで request と limit を同じにすると CPU burst ができなくなるため、CPU は別途考えます。
2. heap 外メモリを見積もる
次に、heap 外メモリを見積もります。
先に MaxRAMPercentage を決めるのではなく、Metaspace、Direct Buffer、Thread Stack、Code Cache、JVM native 領域、monitoring agent などに必要な量をざっくり置きます。
例えば、memory limit 1Gi の普通の Spring Boot Web API なら、最初は次のように置けます。
| 項目 | 仮の見積もり |
|---|---|
| Metaspace / Class | 80Mi |
| Thread Stack | 100Mi |
| Direct Buffer | 64Mi |
| Code Cache | 64Mi |
| JVM native / GC / JIT / agent | 100Mi |
| 安全マージン | 100Mi |
この例では heap 外 + 安全マージンで 508Mi になります。1Gi Pod だと heap は約 516Mi しか残らないため、少し保守的すぎるかもしれません。
実際には、thread 数が少ない、Direct Buffer をあまり使わない、agent が軽い、などが分かってくると、heap 外の見積もりを圧縮できます。
最初の目安としては、次のように考えるとよさそうです。
| 状況 | heap 外 + 安全マージンの目安 | 結果としての MaxRAMPercentage |
|---|---|---|
| 普通の Spring Boot Web API | memory limit の 25% から 35% |
65 から 75 |
| Direct Buffer が多い | memory limit の 30% から 40% |
60 から 70 |
| thread 数が多い | thread数 * Xss を足して考える |
thread 数次第 |
| native library / agent が重い | memory limit の 35% 以上 |
65 以下も候補 |
| memory limit が小さい | 固定費が効くので厚めに残す | 低めから確認 |
3. 残りを heap に割り当てる
heap 外メモリと安全マージンを置いたら、残りを最大ヒープにします。
最大ヒープ = memory limit - heap 外メモリ - 安全マージン
MaxRAMPercentage を使う場合は、その最大ヒープを割合に変換します。
MaxRAMPercentage = 最大ヒープ / memory limit * 100
例えば memory limit が 1Gi、heap 外 + 安全マージンを 300Mi と見るなら、最大ヒープは約 724Mi です。
724 / 1024 * 100 = 約70.7
この場合、MaxRAMPercentage=70 くらいが候補になります。
4. GCログを見る
-Xlog:gc* を入れて、次を見ます。
- GC pause が長すぎないか
- Full GC が出ていないか
- heap 使用量が右肩上がりでないか
- allocation rate が高すぎないか
- GC 後の used heap がどのくらい残るか
GC 後の used heap が最大ヒープに近い状態で張り付いているなら、heap が足りないか、オブジェクトが残りすぎています。
5. container の RSS を見る
heap は余っているのに Pod の memory 使用量が高い場合、heap 外を疑います。
この場合は次を見ます。
- thread 数
- Direct Buffer
- Metaspace
- Code Cache
- monitoring agent
- NMT の結果
特に次の状態なら、heap 外の調査に進みます。
- heap used は余っているのに Pod の memory usage が limit に近い
- GC 後の heap は下がるのに RSS が下がらない
OOMKilledになるが heap dump が残らない- direct buffer や native thread の OOM が出る
6. CPU throttling を見る
latency が悪い場合、GC だけでなく CPU throttling も見ます。
CPU limit が低すぎると、アプリケーション thread だけでなく GC や JIT も影響を受けます。
JVM が CPU 数をどう見ているかは、次で確認します。
java -XshowSettings:system -version
必要であれば ActiveProcessorCount を検討します。
JAVA_TOOL_OPTIONS と JDK_JAVA_OPTIONS
コンテナで JVM options を渡す場合、環境変数を使うことがあります。
よく見るのは次の 2 つです。
JAVA_TOOL_OPTIONS
JDK_JAVA_OPTIONS
JAVA_TOOL_OPTIONS は、Java tools が読み取る環境変数として広く使われています。
JDK_JAVA_OPTIONS は java launcher が読み取り、コマンドライン引数の前に追加されます。Oracle の docs では、-jar のような main class を指定する option や、-h のように実行を終了させる option は指定できないと説明されています。
Kubernetes の manifest では、例えば次のように書けます。
env:
- name: JAVA_TOOL_OPTIONS
value: >-
-XX:MaxRAMPercentage=70
-Xlog:gc*:stdout:time,level,tags
どちらを使うかは base image や起動スクリプトの流儀にもよります。Spring Boot の container image では、entrypoint 側で環境変数を解釈している場合もあるため、実際の起動コマンドに反映されているか確認するのが大事です。
まとめ
Kubernetes で Java Web Server を動かすとき、JVM options は「速くするためのおまじない」ではなく、Pod のリソース境界と JVM の内部設定をつなぐためのものとして見ると理解しやすいです。
まず押さえるポイントは次の通りです。
- memory limit は Java heap だけでなくプロセス全体に効く
MaxRAMPercentageの default は25なので、Web Server では少なすぎる場合がある- heap に寄せすぎると、Direct Buffer、Thread Stack、Metaspace などの余裕がなくなる
- Java の
OutOfMemoryErrorと Kubernetes のOOMKilledは別物 - G1 は default で有効なので、まずは GCログを見てから調整する
- CPU limit がある環境では、GC や JIT も throttling の影響を受ける
最初の設定例としては、次のようなあたりから始めるのがわかりやすそうです。
-XX:MaxRAMPercentage=70
-XX:InitialRAMPercentage=30
-Xlog:os+container=info
-Xlog:gc*:stdout:time,level,tags
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dumps
そこから、GCログ、RSS、OOM の種類、CPU throttling、native memory を見ながら調整していくのがよさそうです。
参考
- Oracle Java 21: The java Command
- Kubernetes: Resource Management for Pods and Containers
- Kubernetes: Pod Quality of Service Classes
- Oracle Java 21: Garbage-First Garbage Collector Tuning
- Oracle Java 21: The Z Garbage Collector
- Oracle Java 22: Native Memory Tracking
- OpenJDK: JDK-8230305 Cgroups v2: Container awareness