- はじめに
- JVMのパフォーマンス設計とは
- JVMとTomcat・OSの関係
- JVM Memoryの全体像
- Heapの仕組み
- Heap Sizeの設計
- Object生成量とLive Set
- Heap以外のMemory設計
- GCの基本的な仕組み
- Stop-the-WorldとResponse Time
- GC方式の選択
- G1 GCの設計
- 低Latencyを重視したGC設計
- System.gc()による明示的なGC
- GC Tuningの進め方
- CPUとJIT Compilationの設計
- Container環境のResource設計
- Memory不足時に何が起こるか
- Memory Leakと過剰なデータ保持
- 障害・縮退時のJVM性能設計
- JVMの性能試験
- JVMの性能設計を確認する
- まとめ
- 参考文献
はじめに
JavaのWebシステムで応答が遅くなると、「Heapを増やす」「GC方式を変更する」といった対策が候補になります。しかし、設定を変えるだけで改善するとは限りません。CPUが不足していればGCも進みにくくなり、Applicationがデータを保持し続けていればHeapを増やしてもいずれ限界に達します。
JVMのパフォーマンス設計で重要なのは、Applicationが生成・保持するデータ量と、CPU・Memoryのリソース配分と応答時間の目標を整合させることです。Throughputを高めたいのか、p99の遅延を抑えたいのかによっても、選ぶ構成は変わります。
本記事では、Tomcatを実行するJVMに視点を置き、Memoryの構造からGC、サイジング、性能試験、調査方法までを解説します。
本記事の前提:Java 21のHotSpot JVMを中心に、Linux / Container上のWeb Applicationを想定します。数値と設定は説明用であり、一律の推奨値ではありません。GCの動作や利用可能なOptionはJDKのVersion・配布元・実行環境で異なるため、導入するJDKで確認してください。
JVMのパフォーマンス設計とは
JVMはJavaのBytecodeを実行し、ObjectのMemory管理やGC、実行コードの最適化などを担います。ApplicationのSQLや計算処理を改善することに加え、それらを実行する基盤にも適切なResourceが必要です。
| 観点 | 設計で決めること | 代表的な指標 |
|---|---|---|
| Throughput | 一定時間に処理したい仕事量 | RPS、Batch処理件数 |
| Latency | 利用者が待てる時間 | p50、p95、p99 |
| Memory | 保持データと一時的な増加を支える容量 | Heap使用量、RSS、Container使用量 |
| CPU | ApplicationとJVM内部処理に配分する計算能力 | CPU時間、使用率、Throttling |
たとえば、夜間Batchは全体の終了時間が重要ですが、対話型APIでは一部のRequestだけが長く遅れることも問題になります。同じApplicationでも、合格条件によって適切なHeapやGC方式は変わります。
最初に「通常時・ピーク時・障害時に何RPS(Requests Per Sec) を、どの応答時間とError率で処理するか」を決めましょう。GC Pauseの目標は、そのサービス要件から検討します。
JVMとTomcat・OSの関係

TomcatとJVMが別々の独立したProcessになるわけではありません。一般的な構成では、TomcatもApplicationも、一つのJava Processの中で動きます。
Tomcatで同時Request数を増やすと、処理中のObjectやThreadが増える可能性があります。DBの応答が遅くなれば、その待ち時間だけRequestに関連するデータが長く残ることもあります。
したがって、JVMの使用量はJVMの設定だけでは決まりません。前段の流入制御、Applicationのデータ保持、Backendの応答時間も含めて考えます。
JVM Memoryの全体像
次の既存図は、Java HeapとNative Memoryの区分を理解するための概念図です。Young内のEden・Survivor(From / To)は従来型の世代別Heapの説明であり、G1やZGCの実際の配置・回収方式をそのまま示すものではありません。G1のRegion構造は後の節で説明します。

図ではThread StackやDirect Buffer、GCの管理領域などを省略しています。また、C HeapはNative Memory全体と同義ではありません。Process全体の容量設計では、次の内訳も含めて確認します。

HeapはJava Processが使うMemoryの一部です。-XmxでHeapを4GiBに制限しても、Process全体が4GiB以内に収まるという意味にはなりません。Heapが溢れてGCが頻発し、性能影響起きるトラブルは典型的な事例なので、要注意です。
| 用語 | 意味 | 注意点 |
|---|---|---|
| Reserved | 仮想Address空間として予約した範囲 | その全量を物理Memoryで使用しているとは限らない |
| Committed | JVMなどが利用可能にした領域 | RSSと同じ値ではない |
| Used | Heapなどで実際に使用している領域 | 観測対象ごとに定義を確認する |
| RSS | Processの物理Memory常駐量 | Heap以外や共有Pageも関係する |
ContainerのMemory使用量も、ProcessのRSSとは測定範囲が異なる場合があります。異なる指標を単純に足し合わせず、どのResourceの上限と比較する数値なのかをそろえます。
Heapの仕組み
Heapには、Applicationが作成するObjectや配列などが置かれます。Requestを処理すると、入力データ、検索結果、変換用の中間データ、Responseなどが生成されます。

GCは「業務上もう不要か」ではなく、参照関係などに基づいて回収可能かを判断します。使わなくなったデータでも、無期限のCacheやCollectionから参照され続けていれば回収できません。
多くのObjectは短期間で不要になりますが、設定情報やCacheなどは長く残ります。この寿命の違いが、世代別GCの考え方につながります。ただし、すべてのGC方式が同じ領域構造や回収手順を使うわけではありません。
Heap使用量が増減すること自体は通常の動作です。上昇する瞬間だけで異常と判断せず、回収後にどの水準へ戻るかを追います。
Heap Sizeの設計
| 設定 | 意味 |
|---|---|
-Xms | 初期・最小Heapサイズ |
-Xmx | 最大Heapサイズ |
Heapを大きくするとObject生成に対する余裕が増え、GC頻度が下がる場合があります。一方で、Process全体で利用できるMemoryの余裕が減ります。
Heapを増やしたときのPause時間は、GC方式や生存Object量などに左右されます。「Heapが大きいほど必ず速い」「大きいほど必ずPauseが長い」とは判断できません。
生存データと余裕を分けて考える

たとえば、代表的な負荷で回収後に1.5GiBが残る環境でも、最大Heapを1.5GiB付近にすると新しいObjectを作る余裕が不足します。必要な余裕は生成速度やGC方式によって異なるため、一律の倍率では決めません。
-Xmsと-Xmxを同じ値にするとHeapサイズの変動を抑えられますが、小さな負荷でもMemoryを確保する構成になります。物理Memoryの使用時期まで完全に固定する意味ではありません。共有環境では、他のProcessとの容量配分も確認します。
Object生成量とLive Set
Heap容量を考える際には、どれだけ速く生成するかと、どれだけ長く残すかを分けて測定します。
| 指標 | 意味 | 設計への影響 |
|---|---|---|
| Allocation Rate | 単位時間あたりのObject割り当て量 | 回収が追いつくためのCPUと空き容量 |
| Live Set | 到達可能で生存しているObjectの集合・量 | 回収してもなくならない容量 |
| 回収後使用量 | あるGC後に残ったHeap使用量 | Live Setを考える手がかり。ただしGCの回収範囲に注意 |

AではCacheや保持期間の見直し、Bでは中間Objectや不要な変換の削減が候補になります。GC直後の使用量も、Young領域だけの回収後なのか、一連のOld領域回収後なのかで意味が異なります。Live Setを調べるために本番で毎回Full GCを起こす運用は避け、同じ条件での時系列と診断情報を使います。
Heap以外のMemory設計
Heap以外にも、同時処理数、Class数、通信方式などに応じてMemoryが必要です。TomcatのThread数を変えた場合は、Process全体の使用量も再評価します。
| 領域 | 増加に関係する要因 | 確認すること |
|---|---|---|
| Metaspace | 読み込むClassやClassLoaderの増加 | 再Deploy後もClassLoaderが残っていないか |
| Thread Stack | Platform Thread数 | Thread数とStack設定、OS上限 |
| Code Cache | JITによるコード生成 | 使用量、コンパイルの状態 |
| Direct Buffer | NIOや通信Library | 同時通信数、Bufferの再利用と解放 |
| Native領域 | GCの管理情報やNative Library | NMTとOS情報の差、負荷による増加 |
仮にPlatform Threadが500本で、Stack設定が1MiBなら、Stackの予約規模は約500MiBです。これが最初から全量RSSに加算されるわけではありません。ThreadにはStack以外の管理情報も必要です。
Stackを小さくしすぎると、深い呼び出しでStackOverflowErrorが発生しやすくなります。Heapを確保するために他の領域を根拠なく削るのではなく、測定結果に基づいて必要なMemoryを配分します。Virtual ThreadではStack管理が異なるため、この概算をそのまま使いません。
Metaspaceでは、不要になったClassLoaderに関連するClassがアンロードされることで領域が回収される場合があります。Threadの大量生成が直接Metaspaceを消費するという関係ではなく、主にStackやThread管理領域を圧迫します。また、Code Cacheが不足しても既存のコンパイル済みコードが一斉にインタプリタ実行へ戻るわけではありません。新規コンパイルの停止や回収・再開の状態をログで確認します。Native Memory全体を「Cヒープ」と同一視せず、Stack、Code Cache、Bufferなどに分けて必要な容量を整理します。
GCの基本的な仕組み
GCは、不要になったObjectのMemoryを回収し、新しいObjectのために再利用できるようにする仕組みです。回収にあたっては生存Objectの識別、移動、参照の調整などの作業が発生します。

世代別GCは、短命Objectと長く生存するObjectを分けて扱うことで、効率的な回収を目指します。YoungとOldという言葉は、この寿命の違いに関係します。
| 言葉 | 意味 |
|---|---|
| Parallel(並列) | 複数のGC Threadで同時に作業する |
| Concurrent(並行) | Applicationを動かしながらGC作業を進める |
| Stop-the-World | Application Threadを一時停止する |
並列GCであっても、Applicationを止めている場合があります。また、並行GCでも短い停止処理はあり得ます。「並列」と「並行」は同じ意味ではありません。
Stop-the-WorldとResponse Time
Stop-the-World(STW)が発生すると、そのJVM内のApplication処理が一時停止します。停止に重なったRequestに加え、その間に到着したRequestにも待ちが発生することがあります。

平均応答時間が良好でも、短時間の停止によって一部のRequestが遅くなる可能性があります。p95は95%、p99は99%のRequestがその時間以内に収まる値です。
ただし、「100msのPauseがあればp99に必ず100ms加算される」という関係ではありません。Pauseの頻度、流入のタイミング、待ち行列、計測期間によって結果は変わります。
また、すべての遅延やSafepoint停止がGCに起因するわけではありません。GC LogとRequestの遅延を同じ時刻で照合し、DB待ちやCPU制限なども切り分けます。
GC方式の選択
GC方式によって、回収中にApplicationを停止する範囲や、GCに必要なCPU・Memoryが異なります。Java 21のHotSpot JVMを前提に、Parallel GC・G1 GC・ZGCを比較します。G1 GCは一般的なサーバー環境における標準のGC方式ですが、サービスの性能要件に応じて測定・選択します。
| 比較項目 | Parallel GC | G1 GC | ZGC |
|---|---|---|---|
| 重視する指標 | Throughput | ThroughputとLatencyの両立 | 低Latency |
| 回収方式 | 複数のGC Threadで並列回収 | Region単位の回収と並行Marking | 主要な回収処理をApplicationと並行実行 |
| STW | Young / Old回収中に停止 | Young / Mixed GCなどで停止 | 短い停止Phaseがある |
| 並行処理 | 通常の回収は基本的にSTW | Markingなどを並行実行 | MarkingやObject移動などを並行実行 |
| CPUの特徴 | GC時に複数Threadを使用 | 停止中と並行処理中の双方で使用 | Application実行中もGCがCPUを使用 |
| 検討例 | Batch処理 | 一般的なWeb Application | 厳しいp99要件のあるAPI |
Parallel GC:複数Threadで一気に回収する
Parallel GCは、Application Threadを停止させ、複数のGC ThreadでYoung領域やOld領域を回収します。回収処理に複数Coreを活用できる一方、回収中はRequest処理が停止します。個々のPauseよりも、一定時間内の処理件数を重視するBatchなどで検討する方式です。

上図は概念図です。GC Threadの本数やPause時間は環境によって変わります。複数Threadで並列に回収していても、Applicationと並行しているわけではない点が重要です。
G1 GC:Region単位で回収対象を選ぶ
G1 GCはHeapをRegionに分割し、Young GCでYoung領域を回収します。並行MarkingでOld領域の生存状況を調べた後、Mixed GCでYoungと一部のOld領域を回収します。Young GCやMixed GCではSTWが発生しますが、Markingなどの一部処理はApplicationと並行して進めます。

実際には並行Markingの開始・終了付近にも停止Phaseがあり、Young GCが途中で発生することもあります。-XX:MaxGCPauseMillisは停止時間の目標であり、上限の保証ではありません。Region構造と設定例は次の「G1 GCの設計」で詳しく説明します。
ZGC:主要な回収処理を並行実行する
ZGCはMarkingやObject移動などの主要な回収処理をApplicationと並行して進め、STWを短く抑えることを目指します。Java 21では世代別ZGCを明示的に有効化できます。停止時間を短縮できても、並行GCがCPUを使用し、回収中もObjectが生成されるため、CPUとHeapの余裕が必要です。

短いPauseだけでなく、並行GC中のCPU使用量、Allocation Stall、Process全体のMemory、同じRPSでのp99を評価します。Java 21の設定例は後の「低Latencyを重視したGC設計」で紹介します。
GC方式を比較するときの評価項目
| 観点 | 測定項目 | 確認すること |
|---|---|---|
| Latency | GC Pause、p95 / p99、Timeout | GCによる停止が応答時間の悪化と重なっているか |
| Throughput | 達成RPS、Batch終了時間 | GC方式変更で業務処理量が低下していないか |
| CPU | Process / GCのCPU時間、Throttling | 並行GCとApplicationがCPUを奪い合っていないか |
| Memory | Heap使用量、回収後使用量、RSS | 回収・Object移動に必要な余裕があるか |
GC方式は、名前だけで優劣を決めず、同じJDK・Application・データ量・負荷条件で比較します。GC Pauseが短くなってもCPU競合で業務処理が遅くなれば、サービス全体の改善とは限りません。また、DB待ちやMemory Leakが原因の場合、GC方式の変更だけでは根本解決しません。
G1 GCの設計
G1はHeapを複数のRegionに分けて扱います。RegionごとにYoung、Old、空きなどの役割があり、回収対象を選びながら処理を進めます。

Young GCでは主にYoung領域を回収します。生存状況を調べる並行Markingなどを経て、Mixed GCではYoungと一部のOld領域を回収します。
Mixed GCが発生するまでの条件
Mixed GCは、Old領域の使用率が一定値を超えた瞬間に実行されるものではありません。まずIHOP(Initiating Heap Occupancy Percent)などを基に並行Markingを開始し、Old Regionの生存状況を調べます。その後、Cleanupで回収候補を評価し、回収する価値がある場合にYoung-onlyの準備段階を経てMixed GCへ移行します。
| 段階 | 何を判断・実行するか | 注意点 |
|---|---|---|
| ① IHOPに基づく開始判断 | Concurrent Start Young GCを契機に並行Markingを開始 | IHOPはMixed GCを直接開始するしきい値ではない |
| ② 並行Marking | Applicationと並行してOld Regionの生存状況を調査 | 開始・終了付近には短いSTW Phaseもある |
| ③ Remark / Cleanup | 生存状況を確定し、Old Regionの回収候補と回収効率を評価 | 回収する価値が小さければMixed GCへ進まない場合がある |
| ④ Prepare Mixed / Mixed GC | Youngと選択したOld RegionをSTWで回収 | 一度ですべてのOld Regionを回収するわけではない |
代表的な関連Optionは-XX:InitiatingHeapOccupancyPercent、-XX:G1MixedGCLiveThresholdPercent、-XX:G1HeapWastePercent、-XX:G1MixedGCCountTargetです。Java 21ではAdaptive IHOPが既定で有効なため、InitiatingHeapOccupancyPercentは主に初期値として使われ、稼働状況に応じて開始しきい値が調整されます。最初から変更せず、GCログで開始時刻、回収候補、Pauseを確認します。
回収のために生存Objectを移動する空きも必要です。Heapが詰まりすぎると通常の回収が難しくなり、Full GCなどの重い処理につながる場合があります。
-XX:+UseG1GC
-Xms2g
-Xmx4g
-XX:MaxGCPauseMillis=200MaxGCPauseMillisはPause時間の目標であり、200ms以下を保証する上限ではありません。厳しくすると1回の回収量や頻度が変わり、Throughputに影響する場合があります。
最初からYoungサイズやRegionサイズを固定せず、基本設定で測定します。大きな配列などのHumongous Objectが問題になっている場合も、先に生成箇所やサイズを確認しましょう。
低Latencyを重視したGC設計
利用者向けAPIで厳しいp99の目標があり、GC Pauseが主要因だと確認できた場合は、ZGCなどの低Latency向けGCを比較する価値があります。
-XX:+UseZGC
-XX:+ZGenerational
-Xms2g
-Xmx4g上記はJava 21で世代別ZGCを選択する例です。G1の設定例とは別の選択肢であり、複数のGC選択Optionを同時に指定しません。
並行して回収する間もApplicationはObjectを生成するため、空きHeapとCPUの余裕が必要です。短いPauseだけを見てHeapを限界まで小さくすると、割り当てが回収に追いつかず待たされる可能性があります。
比較時には、Pauseだけでなく、同じRPSでのp99、CPU、Memory、割り当て停滞、Error率を見ます。DB待ちが遅延の主因なら、GC変更よりDB側の改善が先になります。
System.gc()による明示的なGC
System.gc()はGC方式ではなく、ApplicationからJVMにGCの実行を要求するAPIです。実行が必ず保証されるわけではなく、要求を受けたときの動作はGC方式やJVM Optionで異なります。
System.gc();| 方式・設定 | 明示的GC要求への主な動作 |
|---|---|
| Parallel GC | 通常はSTWを伴うFull GCを実行する |
| G1 GC | 通常はSTWを伴うFull GCを実行する |
| ZGC | 主要な回収処理を並行実行する(短い停止Phaseはあり得る) |
-XX:+DisableExplicitGC | 明示的なGC要求を無効化する |
-XX:+ExplicitGCInvokesConcurrent | G1では明示的GC要求を並行回収で処理する |
商用システムでの定期的なSystem.gc()実行は、原則として避けます。 G1では意図しないFull GCと長いSTWにつながる可能性があります。また、参照が残るObjectは明示的GCでも回収できないため、Memory LeakやHeap容量不足の根本対策にはなりません。
大量の一時Objectを使うBatchの終了後など、明確な目的と十分なアイドル時間がある場合に限り、同じ負荷条件で効果と停止時間を検証して判断します。ライブラリなどによる意図しない呼び出しが疑われる場合は、GCログのSystem.gc()に関するCauseを確認し、必要に応じて上記Optionを検討します。
GC Tuningの進め方
多数のOptionをまとめて変更すると、何が改善や悪化の原因か分からなくなります。変更前の測定結果を残し、仮説ごとに比較します。
- 目標RPS、p95 / p99、Error率、CPU・Memory上限を決める。
- JDK Version、GC方式、起動Option、負荷条件を記録する。
- Warm-up後に基準値を測る。
- Pause、生成量、保持量、CPU制限から原因の仮説を立てる。
- 一つの要因を変更し、同じ条件で再測定する。
- 長時間試験と障害試験で副作用と回復を確認する。
| 観測した問題 | 先に調べること | 対策の候補 |
|---|---|---|
| GC頻度が高い | Object生成量、Heapの余裕 | 不要な生成を削減、容量を再検討 |
| 回収後使用量が高い | Cache、Session、処理中データ | 保持件数・期間の制限 |
| Pauseが長い | GCのPhase、CPU不足、大きなObject | 原因箇所を改善、方式を比較 |
| CPUが高い | ApplicationとGCのCPU配分 | 処理削減、CPUの割り当てを見直す |
Memory LeakやBackend遅延をGC Optionだけで解決しようとしないことも重要です。変更後に目標を満たせたら、細かな設定を増やすより運用時の監視と再現性を整えます。
CPUとJIT Compilationの設計
JVMではApplicationに加え、GCやJIT CompilerもCPUを使います。JITは実行状況を基にコードを機械語へ変換・最適化するため、起動直後と安定稼働後では性能が異なる場合があります。
説明用に、1Requestが平均4msのCPU時間を使い、1,000 RPSを処理するなら、業務処理だけで毎秒4 CPU秒です。概算で4 Coreが常時稼働する量に相当し、GCやJIT、負荷変動の余裕が別途必要です。
性能試験では、起動直後の測定とWarm-up後の測定を分けます。固定時間待つだけではなく、代表的なAPIを実行し、Throughput、CPU、コンパイル状況が落ち着いているかを確認します。
Warm-upにはClass読み込み、Cache構築、接続Pool準備も関係します。CPUが高いからといってすべてJITの問題とせず、JFRなどで内訳を確認します。
Container環境のResource設計
ContainerではHost全体の容量だけでなく、そのContainerに設定されたCPU・Memory制限が重要です。Hostに空きがあっても、制限によって処理が遅くなったりProcessが終了したりします。
Memory上限:8GiBの設計例
Heap最大 4.0GiB
Heap以外のProcess使用量 1.5GiB
その他の計上分 0.5GiB
変動に備える余裕 2.0GiB
─────────────────────────────
合計 8.0GiBこれは配分の説明例です。「Heapを常に50%にする」という推奨ではありません。Container側の計上対象とProcessの内訳をそろえ、同じ領域を二重計上しないようにします。
MaxRAMPercentageなどによるHeapの割合指定も、Process全体をその割合に制限するものではありません。割合を使う場合も、Heap以外に必要な容量を検証します。
CPU制限によるThrottlingが起きると、Applicationだけでなく並行GCも十分に進めなくなる場合があります。CPU使用率に加え、制限値、Throttling時間、JVMが認識するCPU数を確認します。
java -XshowSettings:system -versionこの例はLinuxでJVMが認識するシステム情報を確認する入口です。出力内容は環境によって異なります。起動済みProcessには実際の起動Optionも別途確認します。
Memory不足時に何が起こるか
Memory不足では、直ちにProcessが終了するとは限りません。先にGCが増え、CPUや応答時間が悪化し、最終的にErrorになる場合もあります。

| 症状・表示例 | 主に疑う対象 |
|---|---|
| Java heap space | Heap容量とObject保持 |
| Metaspace | Class MetadataとClassLoader |
| Direct buffer memory | Direct Bufferの使用量・制限 |
| unable to create native thread | Native MemoryやThread数・OS制限 |
| ContainerのOOMKilled | ContainerのMemory上限超過など |
OutOfMemoryErrorは種類ごとに原因を調べます。Heapを増やしても、Native Thread作成やMetaspaceの問題が解消するとは限りません。
OSに強制終了された場合は、JVMがHeap Dumpを残す機会がないこともあります。また、終了コード137だけでOOMと断定せず、Containerの終了理由やOSの記録と照合します。
障害時の証拠を残せるよう、Logの保存先・容量・保持期間と、再起動後も参照できる場所を事前に決めます。
Memory Leakと過剰なデータ保持
Memory Leakを疑う際は、使用量の一時的な上昇より、同じ負荷条件での回収後水準が長期的に増え続けるかを確認します。

ただし、CacheのWarm-upや利用者数の増加でも保持量は増えます。増加が仕様上のデータ量と対応するか、上限に達して安定するかを調べます。
| 保持元 | 確認すること |
|---|---|
| Cache | 最大件数・容量・有効期限 |
| Session | 保持データのサイズと失効 |
| Queue | 投入量が処理量を超えていないか |
| ThreadLocal | Thread再利用時の値の除去 |
| ClassLoader | 再Deploy後の参照残り |
Heap Dumpでは、Object数だけでなく、何が大量のデータを保持しているかを確認します。Retained SizeやGC Rootへの参照経路は、その手がかりになります。小さな管理Objectが大きなデータ群を保持している場合もあります。
Heapを増やすと障害発生を遅らせられることはありますが、保持が無制限なら根本解決にはなりません。上限や解放条件を設計します。
障害・縮退時のJVM性能設計
Tomcat Nodeが停止すると、残ったNodeのRequest数が増えます。それに伴ってObject生成量や処理中データも増え、通常時には余裕があったJVMが逼迫する場合があります。
通常時:3台で合計1,200 RPS
各Node:400 RPS
1台停止:2台で合計1,200 RPS
各Node:600 RPS
1台あたりの処理量:1.5倍Object生成量が単純に比例するとは限りません。待ち時間の増加、Retry、Cache Missなどで、処理中データやCPUの負担がさらに増える場合があります。
縮退試験では、Heap使用量だけでなく、GC頻度、Pause、Process全体のMemory、p99を測ります。CPUやMemoryを使い切る通常時のサイジングでは、障害時の余裕がありません。
復旧時には、Class読み込み、JIT、Cache構築が重なります。必要な準備ができたことを確認して流入させ、段階的に負荷を戻す運用を検討します。再起動できることと、すぐに通常の性能が出ることは別です。
JVMの性能試験
性能試験は、GC単体の数値を良くするためではなく、サービスの要件を満たすために行います。負荷生成側が上限に達していないかも確認します。
| 試験 | 主な確認内容 |
|---|---|
| Warm-up後の通常負荷 | 基準となるCPU・Memory・応答時間 |
| 段階的な負荷増加 | 飽和し始める位置と原因 |
| 突発負荷 | Allocation急増と回復 |
| 長時間負荷 | 回収後使用量やNative Memoryの増加 |
| Backend遅延 | 処理中データの保持とTimeout |
| Node停止・復旧 | 縮退時の余裕とWarm-up |
合格条件を先に決める
たとえば「800 RPSでp95が300ms以下、p99が1秒以下、Error率0.1%未満」を業務側の合格条件とし、Memoryが時間とともに増え続けないことや、負荷減少後に回復することも確認します。数値は説明用です。
| 観測対象 | 記録する指標 |
|---|---|
| サービス | 達成RPS、p50 / p95 / p99、Error、Timeout |
| Heap | Used、Committed、上限、同じ回収段階の使用量 |
| Object | Allocation Rate、保持量の変化 |
| GC | Pause分布、回数、累積停止時間、Full GC |
| Process / Container | CPU時間、Throttling、RSS、Memory計上値 |
GC Pauseの累積時間と、並行処理を含むGCのCPU時間は異なります。停止が短くてもGCのCPU負担が大きい場合があるため、両方を評価します。
同じJDK、データ量、API構成、負荷方式で比較し、測定を複数回行ってばらつきを確認します。平均値だけでなく分布と時間推移を残しましょう。
JVMの性能設計を確認する
ここではJava 21の診断例を示します。<PID>は対象Java ProcessのIDに置き換えます。実行権限やContainer内外のProcess識別を確認し、利用可能なコマンドはjcmd <PID> helpで調べます。
GC Logで停止と回収の推移を見る
-Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20M起動時に指定する例です。保存先は事前に作成し、実行ユーザーの書き込み権限を確認します。GCの種類、Pause時間、回収前後の使用量を、Applicationの遅延と照合します。Safepoint Logも併用します。
GCログでYoung・Mixed・Full GCを区別する
G1のGCログでは、GC Causeと回収種別を見て、通常のYoung GC、Mixed GC、Full GCを区別します。以下はログに現れる表記の説明用例であり、実測ログではありません。実際の出力はJDKやログ設定によって異なります。
Pause Young (Normal) (G1 Evacuation Pause)
Pause Young (Mixed) (G1 Evacuation Pause)
Pause Full (System.gc())
Pause Full (Diagnostic Command)| ログの表記例 | 読み方 |
|---|---|
Pause Young (Normal) | 通常のYoung GC。Young領域を回収 |
Pause Young (Mixed) | Mixed GC。Youngと選択されたOld Regionを回収 |
Pause Full (System.gc()) | 明示的GC要求に伴うFull GCの例 |
Pause Full (Diagnostic Command) | 診断コマンドを契機とするFull GCの例 |
Mixed GCが発生しない場合は、並行Markingが開始・完了しているか、Cleanup後に回収候補が残っているかを確認します。Full GCが見つかった場合も、すぐにHeap不足と決めず、ログに記録されたCauseと直前のHeap・CPU・応答時間を照合します。
JMXで継続監視する
Memory Pool、GarbageCollector、Thread、ClassLoadingなどの情報を時系列で取得します。GC方式によってPool名や取得できる意味が異なるため、監視設定を変更後も確認します。リモート接続する場合は認証と接続制限を設定します。
JFR / JMCで実行時の原因を調べる
jcmd <PID> JFR.start name=perf settings=profile duration=60s filename=/var/log/app/perf.jfr60秒間の記録例です。JMCなどでCPU、Lock、GC、Object割り当てのサンプルを調べます。記録されるEventは設定によって異なり、サンプルがすべての割り当てを網羅するわけではありません。
Thread Dumpで待ちを確認する
jcmd <PID> Thread.print -lCPU処理、Lock待ち、DBや外部API待ちをStackから確認します。必要に応じて間隔を空けて複数回取得し、一時的な状態と継続する状態を区別します。
Heap Dumpで保持元を調べる
jcmd <PID> GC.heap_dump /var/log/app/heap.hprof取得には停止や大きなI/O負荷が発生し得ます。このコマンドは通常、Dump取得前にFull GCを要求します。-allを指定すると到達不能Objectも含めて取得し、Full GCの実行を抑制できますが、取得時の停止やI/O負荷がなくなるわけではありません。対象の規模と影響を確認して実行します。出力先には十分な容量が必要で、Dumpには認証情報や業務データが含まれ得るため、保管・共有範囲を管理します。
Native Memory TrackingでHeap外を調べる
-XX:NativeMemoryTracking=summaryJVM起動時に有効化したうえで、次のように確認します。
jcmd <PID> VM.native_memory summary
jcmd <PID> VM.native_memory baseline
# 負荷試験などを行った後に差分を確認
jcmd <PID> VM.native_memory summary.diffNMTはJVMが追跡できる領域を調べる仕組みであり、第三者のNativeコードなどを含むProcessのすべてを追跡するものではありません。ReservedとCommittedを区別し、OSの情報と併用します。有効化には負荷があるため、試験計画に含めます。
既存の運用手順を性能設計の検証に使う
性能問題の解析は、事象の明確化、OS、JVM、ログ、ダンプの順に情報をそろえると進めやすくなります。性能試験でも同じ順序を使い、設定変更の前後でボトルネックがどう変化したかを確認します。ただし、短時間で消える事象では、ログやJFRなどの証拠取得を先に行う場合もあります。
| 確認の順序 | 確認する情報 | 設計へ反映すること |
|---|---|---|
| 1. 事象の明確化 | 発生時刻、対象API、RPS、応答時間 | 再現条件と合格条件 |
| 2. OS / Container | CPU、Memory、I/O、Throttling | Resource上限と余裕 |
| 3. JVM / Application | Heap、Thread、GC、接続Pool | 容量と同時実行数 |
| 4. ログ | 業務処理・SQL・外部API・GCの時刻 | 遅延の発生箇所と許容時間 |
| 5. 詳細解析 | JFR、Thread Dump、Heap Dumpなど | 生成・保持・競合の改善 |
OS側の調査手順は、LinuxのCPU使用率が高い原因と対処法|商用システムにおける実践的トラブルシューティングも参照してください。
GCログの実例を設計値と照合する
次は、既存の検証で診断コマンドによって意図的にFull GCを発生させたログの抜粋です。自然に発生した容量不足のログとは区別して読みます。
[2025-12-07T08:13:34.555+0900][info ][gc,start] GC(173) Pause Full (Diagnostic Command)
[2025-12-07T08:13:34.680+0900][info ][gc,heap] GC(173) Eden regions: 25->0(98)
[2025-12-07T08:13:34.680+0900][info ][gc,heap] GC(173) Survivor regions: 3->0(14)
[2025-12-07T08:13:34.680+0900][info ][gc,heap] GC(173) Old regions: 49->49
[2025-12-07T08:13:34.680+0900][info ][gc,heap] GC(173) Humongous regions: 0->0
[2025-12-07T08:13:34.680+0900][info ][gc] GC(173) Pause Full (Diagnostic Command) 75M->47M(164M) 124.598ms
[2025-12-07T08:13:34.680+0900][info ][gc,cpu] GC(173) User=0.33s Sys=0.02s Real=0.12s| ログの値 | 読み取れること | 設計上の確認 |
|---|---|---|
| Diagnostic Command | 診断コマンドによるGC | 容量不足によるGCと混同しない |
| 75M → 47M | この回収で使用量が約28M減った | 長期的な保持量と負荷に対する余裕を確認する |
| (164M) | その時点でログに示されたHeap容量 | 最大Heap設定とは別に読む |
| 124.598ms | このFull GCのPause時間 | APIの応答時間要件と発生頻度で評価する |
| User / Sys | GCに関連するユーザー / システムCPU時間 | CPU使用率そのものとして扱わない |
| Real | 実経過時間 | CPU時間と比較して実行状況を調べる |
約28Mを回収したことは確認できますが、この1回だけでメモリリークがないとは判断できません。また、約125msのPauseが許容できるかはサービス要件次第です。100ms以内の応答が必要なAPIなら、重大な影響になり得ます。「1秒未満なら正常」といった固定の基準ではなく、p95 / p99や累積停止時間と照合します。
複数のGC Threadが並列に動く場合、CPU時間の合計が実経過時間を上回ることがあります。Old Region数が変わらないことや、Metaspaceが減らないことだけでも異常とは断定できません。回収対象、生存データ、複数回のログを合わせて判断します。
Tomcat起動時にGCログを有効にする
標準の起動スクリプトを使用するTomcatでは、$CATALINA_BASE/bin/setenv.shのCATALINA_OPTSへ次のように追加できます。先ほどのGCログOptionをTomcatへ渡す例であり、同じ出力先の設定を重複して追加する必要はありません。
CATALINA_OPTS="$CATALINA_OPTS -Xlog:gc*,safepoint:file=$CATALINA_BASE/logs/gc.log:time,uptime,level,tags:filecount=5,filesize=10M"
export CATALINA_OPTSこの例は保存先のパスに空白がない構成を想定しています。変更は再起動後に反映されます。ログディレクトリの書き込み権限と保存容量を確認してください。サービスやContainerからJavaを直接起動する構成では、実際の起動設定へOptionを追加します。
JMXでThread数を測定する具体例
JMXTermで対象JVMへ接続済みの場合、次の操作で現在のThread数とピークを確認できます。
bean java.lang:type=Threading
get ThreadCount
get PeakThreadCountThreadCountは現在生存しているPlatform Thread数、PeakThreadCountはJVM起動後またはピーク値リセット後の最大値です。Tomcatの使用中Worker数や、CPUで実行中のThread数とは一致しません。Java 21のThreadMXBeanはVirtual Threadを計数対象に含めません。
RUNNABLEのThread数もCPU使用中の本数とは限りません。状態別の集計にはThread Dumpのほか、JMXのThreadMXBeanからThreadInfoを取得する方法もあります。「JMXではThread状態を調べられない」と一括りにしないことが大切です。
JMXの接続準備は、TomcatでJMXを有効化する方法|SSL・認証・監視ツール連携で解説しています。
ダンプの取得・解析をさらに詳しく確認したい場合は、Javaパフォーマンス問題を徹底解析|ヒープダンプ・スレッドダンプ・コアダンプの取得と解析方法を参照してください。Core DumpはJVMクラッシュやNativeコードの問題を調べる際の選択肢であり、通常のHeap容量調整で必ず必要なものではありません。
まとめ
JVMのパフォーマンス設計では、Heapサイズだけでなく、Object生成量、保持データ量、GCの処理能力、CPUとProcess全体のMemory容量を整合させます。

| 設計項目 | 確認すること |
|---|---|
| Heap | Live Setと変動、回収用の余裕があるか |
| Heap以外 | Thread・Class・Bufferも必要容量に含めているか |
| GC | PauseだけでなくCPUとThroughputを評価し、Mixed GCと明示的GCの発生状況を確認したか |
| CPU / JIT | 通常時とWarm-upを分けて測ったか |
| Container | 実際のCPU・Memory制限で検証したか |
| 障害時 | 残存Nodeと復旧Nodeに余裕があるか |
| 運用 | Log、監視、Dump取得の手順を準備したか |
Heapを増やす前に、何が残っているかを調べる。GCを変える前に、遅延の原因を確かめる。CPUやMemoryを決めるときは、通常時だけでなくピークと障害時も測る。
この順序で設計と検証を進めることで、JVMの設定を根拠のあるものにできます。Chapter 7のTomcat設計と組み合わせ、受付から実行基盤まで一貫した性能目標とリソース配分を定めることが、安定したWebシステムにつながります。


コメント