状態:ドラフト(2026-10-26)、実測反映済み。 実測環境は2つあります。①ノートPC上のコンテナ(事前に取得した eclipse-temurin:21 イメージ、JDK 21.0.12、ネットワークなし)でサンプルプログラムを5つ自作して実行しました。新しくダウンロードやインストールを行ったものはありません。②TT Labクラスターで稼働中のKeycloak Pod(Red Hatビルド OpenJDK 21.0.11)に対して、 読み取りコマンドのみ(
jcmd 1 VM.versionを実行しました。デバッグポートを開くこと、JFRの記録を開始すること、ヒープダンプを取得すること、再起動することは本番Podでは行っていません。async-profilerはダウンロードが必要なため 未実測です。公式ドキュメントは同日に原文を開いて確認しました。 作成ルール:実測データを基本とし、測定できなかったものは「未実測」、確認できなかったものは「確認できず」、推測は「推定」と記載します。
目次
- 1. 3つの見方
- 2. JVMの内部を覗き見る2つの経路
- 3. デバッガで止めて見る:jdbを一巡する
- 4. Kubernetes Podをリモートデバッグするには
- 5. 止めずに見る:jcmd
- 6. 事例1:停止とデッドロック
- 7. 事例2:CPUが高い
- 8. 事例3: メモリが増え続ける
- 9. JFR:常に有効にしておくフライトレコーダ
- 10. 本番Podでの手順と注意点
- 11. まとめ
- 参考
1. 3つの見方
Javaサービスに異常が発生した際に行えることは大きく分けて3つあります。
- 止めて見る。 デバッガで実行を一時停止させ、1行ずつ進めながら変数を確認します。最も多くの情報を得られますが、停止させている間はサービスも停止します。開発環境向けのツールです。
- 止めずに見る。 稼働中のJVMにアタッチし、スレッド、ヒープ、設定をある瞬間にスナップショットとして取得します(jcmd・jstack)。本番環境で使用する一次ツールです。ごく短時間JVMを停止させるコマンドもあるため、何がその動作を引き起こすのかを把握しておく必要があります。
- 記録して見る。 常に有効にしておく軽量な記録(GCログ、JFR)を後から読み解きます。「昨日の深夜3時になぜ遅かったのか」といった過去の出来事は、この方法でのみ確認できます。
この記事では、まずJVMが外部ツールと対話する2つの経路を図解で確認し(第2節)、デバッガ(第3〜4節)、診断コマンド(第5節)、3つの事例(第6〜8節)、JFR(第9節)、本番環境での手順と注意点(第10節)の順に解説します。
2. JVMの内部を覗き見る2つの経路
デバッガの経路:JPDA Javaプラットフォームデバッガアーキテクチャ(JPDA)は3つの層で構成されています(公式ドキュメント)。
| 層 | 位置づけ | 役割 |
|---|---|---|
| JVM TI | 対象JVM内のネイティブインターフェース | 「このスレッドを停止せよ」、「このメソッドにブレークポイントを設定せよ」、「このローカル変数の値」といったデバッグサービスをJVMが提供 |
| JDWP | 通信プロトコル | デバッガと対象JVM間のバイトプロトコル。ソケットまたは共有メモリを使用 |
| JDI | デバッガ側のJava API | IDEやjdbが使用する高水準インターフェース |
-agentlib:jdwp=… は、対象JVMにJDWPエージェント(ネイティブライブラリ)をロードし、JVM TIを呼び出してJDWPで通信させます。よく使われる設定を紐解くと以下のようになります(JDK 21ドキュメント)。
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
│ │ │ └ 어디서 기다리나. 호스트를 비우면 루프백만, * 면 모든 인터페이스
│ │ └ 시작하자마자 멈추고 디버거를 기다릴까(기본 y!)
│ └ JVM 이 듣는 쪽(서버)이 될까(기본 n: JVM 이 디버거로 걸어 나간다)
└ TCP 소켓
suspend のデフォルト値が y であることを忘れていると、デバッグオプションを付与して起動したサービスが デバッガがアタッチされるまで起動すらしない という状況になります。Kubernetes環境であれば、プローブ(Liveness/Readiness)が失敗し続けます。アドレスの仕様はJDK 9で変更されました。以前はポートを指定するだけですべてのインターフェースで待ち受けていましたが、JDK 9以降はループバックでのみ待ち受けるようになり、すべてのインターフェースでの待ち受けが必要な場合は *: を付与する必要があります。JDK 9のリリースノートには、 * へのバインドは「安全ではなく、推奨されない」と記載されています。
診断コマンドの経路:Attach jcmd·jstack·jmap はネットワークを使用しません。対象JVMの一時ディレクトリにあるUnixドメインソケット(.java_pid<PID>)にアタッチし、JVMは最初のリクエストを受け取ると Attach Listener というスレッドを起動してコマンドを実行します。同じユーザー、同じホスト(コンテナの場合は同じコンテナ)からのみ実行可能です。そのため、Kubernetesでは kubectl exec を使ってそのコンテナ内に進入し、実行します。
3. デバッガで止めて見る:jdbを一巡する
1秒ごとに1点ずつスコアを記録する小さなプログラム(Rally.point(boolean leftWins, int rallyNo))を -g でコンパイルし、JDWPを有効にして起動した後、同じコンテナ内からjdbでアタッチしました(2026-10-26)。
$ jdb -attach 5005 -sourcepath /w
> stop in Rally.point
Breakpoint hit: "thread=main", Rally.point(), line=6 bci=0
6 if (leftWins) left++; else right++;
main[1] where
[1] Rally.point (Rally.java:6)
[2] Rally.main (Rally.java:13)
main[1] locals
Method arguments:
leftWins = false
rallyNo = 6
main[1] print Rally.left ← 정적 필드는 클래스 이름을 붙여야 한다(그냥 left 는 「Name unknown」)
Rally.left = 4
main[1] set leftWins = !leftWins
leftWins = !leftWins = true
main[1] clear Rally.point
main[1] cont
プログラム側の出力を時刻とともに確認すると、デバッガが何を行ったのかがわかります。
| 時刻(秒) | 出力 | 説明 |
|---|---|---|
| :34.03 | ラリー 5 → 4:1 | 通常通りの1秒間隔 |
| :39.54 | ラリー 6 → 5:1 | ブレークポイントで 4.5秒間停止し、本来は右側の得点(4:2)になるはずのラリーを左側の得点に変更した |
| :42.05 | Listening for transport dt_socket at address: 5005 |
jdbを終了すると 待ち受けを再開 |
デバッガは実行を停止させるだけでなく、 値を変更することもできます。 そして、デバッガが切断されてもポートは次の接続を待ち続けます。この2つの事実が、次の節で述べるセキュリティ上の問題につながります。
IDEのリモートデバッグも原理は同じです。IntelliJの「Remote JVM Debug」やVS Codeの "request": "attach" の設定が、上記のjdbの位置でJDIを介して同じJDWPポートにアタッチします。
デバッグポートには認証がない
JDWP接続は、双方が JDWP-Handshake の14バイトをやり取りすることから始まります。それだけです。実際に検証してみました。ポートにアタッチして14バイトを送信し、「VMのバージョンを教えてほしい」というコマンドパケット(11バイト)を1つ送信しました。
JDWP-Handshake
…Java Debug Wire Protocol (Reference Implementation) version 21.0
JVM Debug Interface version 21.0
JVM version 21.0.12 (OpenJDK 64-Bit Server VM, mixed mode, sharing)…
パスワードも証明書も要求されませんでした。これは、デバッガが行えること(任意のメソッドの呼び出し、値の変更)を、ポートに到達できる誰でも行えるということを意味します。JDWPに認証がないと断言する公式ドキュメントの文言は見つかりませんでしたが(確認できず)、上記の実測結果、JDK 9リリースノートの警告、OpenJDKメーリングリストにおける「リモートコード実行を許可する」という説明は同じ方向を向いています。実測したコンテナ内では、 address=*:5005 のソケットが /proc/net/tcp6 の全アドレス(::)の5005番ポートで立ち上がっていました。
コストはどうでしょうか? デバッガがアタッチされていない状態であれば、JDWPエージェントをロードしているだけのコストは測定できないほど小さかったです(第9節の表:基準値比 -0.4%、誤差の範囲)。そのため、「切るのが面倒くだから」という理由で本番イメージにそのまま残りやすく、それが危険を招きます。
4. Kubernetes Podをリモートデバッグするには
本番Podでは行わないのが原則です。ブレークポイントがサービスのスレッドを停止させるとその間にリクエストが蓄積され、Livenessプローブが失敗してkubeletがコンテナを終了させてしまいます。開発やステージング環境で行う際の安全な構成は以下の通りです。
- デバッグ用のコピーを別途立ち上げる。 本番のDeploymentを修正せず、同じイメージでデバッグ専用のPodを作成します(
kubectl debug <파드> --copy-to=<사본>または個別のマニフェスト)。環境変数JAVA_TOOL_OPTIONSにJDWPオプションを指定すれば、イメージの起動スクリプトを変更しなくてもJVMが読み込みます。 - ループバックにのみバインドする。
address=127.0.0.1:5005のように、Pod内のループバックでのみ待ち受けるようにし、Pod外に向けたServiceは作成しません。 - port-forwardで自PCのみに公開する。
kubectl port-forward pod/<사본> 5005:5005はデフォルトで自分のPCのlocalhostにのみオープンされ(公式ドキュメント:--addressデフォルト値はlocalhost)、TCPのみをサポートします。APIサーバーの認証・認可(RBACのpods/portforward)を通過するため、JDWP自体には備わっていない認証をKubernetesが肩代わりしてくれることになります。 - プローブを無効化するか、タイムアウトを延長する。 コピー環境ではlivenessプローブを外し、IDEのブレークポイントは「すべてのスレッド」ではなく「そのスレッドのみ」を停止させるように設定します。
- 終了したらコピーを削除する。
この節の手順は、本番Podではデバッグポートを開かないという本書の原則に従い、 未実測です。port-forwardがPod内のループバックアドレスに到達するという点については、Kubernetesドキュメントの例(Podのポートへの転送)でのみ確認しています。
5. 止めずに見る:jcmd
jcmd <PID> help を実行すると、そのJVMが受け付け可能なコマンドがすべて表示されます。頻繁に使用するものと、 JVMにかかる負荷を併せて記載します(負荷は実測とドキュメントに基づき判断)。
| コマンド | 表示内容 | 負荷 |
|---|---|---|
VM.version · VM.uptime · VM.flags |
バージョン、稼働時間、実際に適用されたJVMオプション | ほぼなし |
Thread.print (= jstack) |
全スレッドのスタックとロック、デッドロック検出 | 短い停止(セーフポイント)。実測89ms(コマンド全体の往復) |
GC.heap_info |
ヒープ領域ごとの使用量 | ほぼなし |
GC.class_histogram |
クラスごとのオブジェクト数・バイト数 | Full GCを引き起こす(第8節の実測) |
GC.heap_dump <파일> |
ヒープ全体をHPROFファイルに出力 | JVMの停止、ヒープサイズ分のディスク領域 |
JFR.start · JFR.dump · JFR.stop |
フライトレコーダの開始・保存・終了 | デフォルト設定で1%前後(第9節) |
VM.native_memory summary |
ヒープ外メモリ(スレッドスタック、メタスペース等) | 起動時に -XX:NativeMemoryTracking を有効にした場合のみ |
Thread.dump_to_file |
仮想スレッドを含むダンプ(JSON出力可能) | JEP 444の例に記載、JDK 21のjcmdマニュアルにはなくJDK 25のマニュアルに存在 |
実測:コンテナ内のJVMはメモリをいくらと認識しているか
JVMはコンテナのメモリ制限(cgroup)を読み取ってデフォルトのヒープサイズを決定します。デフォルトはその制限の25%(-XX:MaxRAMPercentage のデフォルト値、公式ドキュメント)です。ノートPC上のコンテナにメモリ制限を渡し、 -XX:+PrintFlagsFinal で最大ヒープサイズを読み取りました。
| コンテナメモリ | オプションなし | -XX:MaxRAMPercentage=70 |
|---|---|---|
| 512MiB | 128MiB | 360MiB |
| 2GiB | 512MiB | 1,434MiB |
オプションを指定せずに2GiBのPodで起動した場合、ヒープは512MiBだけで残り1.5GiBは遊ぶことになります。逆に割合を高く設定しすぎると、ヒープ外メモリ(スレッドスタック、メタスペース、ダイレクトバッファ)が不足してコンテナがOOMKilledになります。この場合、JavaのOutOfMemoryErrorもヒープダンプも残らず、Podだけが消滅します。
実測:クラスターのKeycloak Podを読み取りのみで確認する
TT LabクラスターのKeycloak Podに kubectl exec で入り、jcmdを実行しました(2026-10-26)。このイメージはRed Hatのheadless JREを使用していますが、その中にも jcmd が含まれていました(jstack·jmap はなし)。jcmd一つで両方の役割をこなすため問題ありません。JVMのPIDは1でした。
| コマンド | 結果 |
|---|---|
VM.version |
OpenJDK 64-Bit Server VM 21.0.11+10-LTS |
VM.uptime |
711,384秒(約8.2日) |
VM.flags(一部) |
MaxRAM=2147483648(ポッド制限 2GiB)、 MaxRAMPercentage=70 → MaxHeapSize=1503657984(1,434MiB)、 UseG1GC, ExitOnOutOfMemoryError, FlightRecorderOptions=stackdepth=512 |
GC.heap_info |
G1ヒープ 177MiB確保中、157MiB使用、メタスペース 141MiB |
Thread.print 要約 |
スレッド 95個(Java状態あり:RUNNABLE 40、TIMED_WAITING 15、WAITING 14)。デッドロックなし。最も多いのは vert.x-eventloop-thread-N 22個 |
JFR.check |
進行中の記録なし |
VM.native_memory summary |
「Native memory tracking is not enabled」 |
いくつかの項目が読み取れます。イメージで ExitOnOutOfMemoryError を有効にしておいたため、ヒープがあふれるとJVMが直ちに終了し、Kubernetesが再起動します。その代わり、原因を残すヒープダンプオプションはありません。最大ヒープ1,434MiBに比べて実使用量は157MiBと余裕が大きいです。上の表の2GiB・70%の計算(1,434MiB)と稼働ポッドの値が完全に一致していることも確認できます。
コマンドを1回実行するたびにAttach Listenerスレッドが生成される点は覚えておきます。「読み取りコマンド」であっても、対象JVM内にスレッドを1つ作成する処理です。
6. ケース1:停止とデッドロック
2つのスレッドが2つのロックを逆の順序で取得する古典的な例(rally-A は table → net、 rally-B は net → table)を起動し、2秒後に jstack 1 を取得しました。
Found one Java-level deadlock:
=============================
"rally-A":
waiting to lock monitor 0x0000ffff18001d30 (object 0x000000008bb13c80, a java.lang.Object),
which is held by "rally-B"
"rally-B":
waiting to lock monitor 0x0000ffff1c000f00 (object 0x000000008bb13c70, a java.lang.Object),
which is held by "rally-A"
…
"rally-A":
at Deadlock.grab(Deadlock.java:10)
- waiting to lock <0x000000008bb13c80> (a java.lang.Object)
- locked <0x000000008bb13c70> (a java.lang.Object)
…
Found 1 deadlock.
JVMが自ら循環参照を見つけ出します(公式のトラブルシューティングガイドの例と同じ形状)。2つのスレッドのCPU時間はそれぞれ1.01ms、0.60msであり、停止したサービスがCPUを使用していないこともわかります。実際の現場での停止は、デッドロックよりも 遅い何かを待っている状態(DBコネクションプールの枯渇、外部HTTP呼び出しのタイムアウト設定なし)の方がより一般的です。そのため、ダンプは 5〜10秒間隔で3回 取得し、同じスレッドが同じ行に留まり続けているかを確認します。1枚だけでは、たまたまその瞬間そこにいただけなのかどうか判断できません。
7. ケース2:CPU使用率が高い
1つのスレッド(referee-worker)だけが計算を実行し、残りの4つのスレッドはスリープする例です。手順は どのスレッドか → そのスレッドが何をしているかとなります。
- コンテナ内で
top -H -p 1を実行してスレッド別のCPU使用率を確認します。実測:TID 24が90.9%、名前はreferee+(切り捨て)。 jstack 1でそのTIDを探します。スレッド行のnidがOSスレッド番号です。
"referee-worker" #20 [24] … cpu=3922.52ms elapsed=3.96s … nid=24 runnable
java.lang.Thread.State: RUNNABLE
at Hot.scoreRally(Hot.java:7)
実測したJDK 21は nid=24 のように 10進数で記載し、 [24] も併記して表示します。JDK 8・11時代のガイドでは nid=0x18 のような16進数だったため、TIDを16進数に変換して探すよう案内されています。そのため、使用しているバージョンに合わせて読み替える必要があります。
- スタックの1枚はあくまで瞬間的なものです。「概ねどこに時間を費やしているか」はJFRで確認します。稼働中のJVMに対して 再起動なしで 記録を開始しました。
$ jcmd 1 JFR.start name=hot duration=10s filename=/w/hot.jfr
$ jfr view hot-methods /w/hot.jfr
Method Samples Percent
Hot.scoreRally(long) 460 100.00%
10秒間の実行サンプル460個(デフォルト設定は約20ms間隔)がすべて同じメソッドでした。同じ記録の jfr view cpu-load ではJVMユーザーCPUの平均が9.02%でしたが、これは マシン全体(10コア)に対する 値であり、「1つのコアを完全に使い切っている」ことを意味します。パーセンテージが何に対する割合なのかを常に確認します。
async-profiler(最新のv4.6)は、JVMの内部API(AsyncGetCallTrace)とLinuxのperf_eventsを使用し、セーフポイントの偏りなしにJavaとネイティブのスタックを同時に取得できるツールです(公式README)。フレイムグラフを直接生成できるためCPUのトラブルシューティングに特に優れていますが、ダウンロードしてポッドに配置する必要があるため、今回は 未実測となります。
8. ケース3:メモリが増加し続ける
64KBのフレームをリストに追加し続けるだけで削除しない「キャッシュ」を、最大ヒープ64MBで起動しました。オプションは本番環境で推奨される組み合わせです。
java -Xmx64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/w/heap.hprof \
-XX:+ExitOnOutOfMemoryError "-Xlog:gc*:file=/w/gc.log:uptime,level,tags" -cp classes Leak
-Xlog:gc* はJDK 9のUnified Logging(JEP 158)と統合GCロギング(JEP 271)によって導入されたフォーマットです。結果は以下の通りでした。
| 時刻 | 標準出力 / GCログ |
|---|---|
| 1.9秒 | Pause Young (Normal) 23M->22M(64M) 8.5ms — Young世代をクリアしたが、ほとんど減らなかった |
| 2.3秒 | Pause Full (Heap Inspection Initiated GC) 27M->26M — 自分が実行した GC.class_histogram が引き起こしたFull GC |
| 3.2〜5.3秒 | YoungとConcurrent Markが繰り返し実行され、使用量が36M → 59Mへと増加し続ける |
| 5.45〜5.55秒 | Evacuation Failure、続いて Pause Full (G1 Compaction Pause) 59M->59M が3回 — Full GCを実行しても1バイトも減らない |
| 5.5秒 | OutOfMemoryError: Java heap space、ヒープダンプ65,304,426バイトを0.073秒で出力、終了コード3 |
読み方は一つです。 Full GCの後でも使用量が下がらない場合、生きている何かがオブジェクトを保持し続けている。 これが単なるアロケーションの多さとメモリリークを切り分ける基準です。2秒時点のクラスヒストグラムは、犯人の姿まで示していました。 [B(バイト配列)の9,728個が26,419,600バイトで1位でした。ただし、そのコマンド自体がFull GCを引き起こしたことが実測として記録に残りました(2.3秒目の行)。大きなヒープを持つ本番サービスで「軽く一度確認してみよう」と実行するようなコマンドではありません。
ヒープダンプから「誰がそれらの配列を保持しているのか」(支配ツリー)を特定するにはEclipse MATやVisualVMなどの分析ツールが必要となるため、今回は 未実測です。実測により確認された本番運用上の事実は2つあります。ダンプファイルは ヒープサイズと同等の大きさ(64MBのヒープに対して65MB)になるため、1.4GBのヒープを持つポッドであれば同等の書き込み可能なボリュームが必要になります。また、ダンプにはメモリ上のすべてのデータ、すなわちユーザー情報、トークン、パスワードなどがそのまま含まれるため、機密情報として取り扱う必要があります。
9. JFR:常時有効にするフライトレコーダー
JFR(JDK Flight Recorder)はJDK 11でオープンソース化され(JEP 328)、デフォルト設定の目標オーバーヘッドは1%以下です。JDK 21の default.jfc は「本番環境での常時稼働も安全、通常1%未満」、 profile.jfc は「通常2%前後」と自ら記載しています。本当にそうであるか、ハッシュ計算と短命なオブジェクトのアロケーションを組み合わせた処理を指定回数(400万回)実行して測定しました。4つの設定を交互に7回ずつ、コンテナの2コアに制限して実行しました。
| 設定 | メインループ時間の中央値(7回) | 基準との比較 |
|---|---|---|
| 基準(オプションなし) | 1,734ms | — |
JFR default |
1,750ms | +0.9% |
JFR profile |
1,753ms | +1.1% |
| JDWPエージェントのみ(デバッガー未接続) | 1,727ms | −0.4% |
基準となる7回の測定範囲が1,713〜1,784msとすでに±2%程度の変動があるため、上記の差は「ドキュメントにある1%・2%の範囲内」であるとしか言えません。処理の特性(アロケーションが非常に多い、ロック競合が激しいなど)によって異なるため、ご自身のサービスで一度測定してみることをお勧めします。
JFRファイルには環境変数が含まれている
上記の実測記録の jfr summary には jdk.InitialEnvironmentVariable が11件、 jdk.InitialSystemProperty が15件含まれており、 jfr print で開くと のキーと値がそのまま 露出していました。KubernetesのポッドはDBのパスワードやAPIキーを環境変数として受け取るケースが多いため、JFRファイルをチケットに添付したり外部に送信したりする前に削除する必要があります。JDK 21の jfr scrub がその処理を行います。
$ jfr scrub --exclude-events jdk.InitialEnvironmentVariable,jdk.InitialSystemProperty hot.jfr hot-clean.jfr
実測:整理されたファイルでは該当する2つのイベントが0件となり、実行サンプル460件はそのまま維持されていました(211,772バイト → 201,397バイト)。
本番環境でのJFRの活用パターン
- 常時有効にする:起動オプションに
-XX:StartFlightRecording=settings=default,maxage=6h,disk=true,filename=/tmp/jfr/のように期間を指定してリングバッファとして実行します。問題発生後にjcmd 1 JFR.dumpを使用して過去数時間のデータを取得します。「過去の出来事」を調査する唯一の方法です。 - 必要な時だけ有効にする:セクション6〜7のように
jcmd 1 JFR.start duration=60sを用いて再起動なしで有効化します。 - 読み取り:
jfr summary,jfr view hot-methods·gc·allocation-by-class、またはJDK Mission Controlで開きます。
10. 本番ポッドにおける手順と注意事項
症状別に最初に確認すべき事項をまとめると図2のようになります。手順の原則は 負荷の少ないものからです。
- すでに残っているデータ:アプリケーションログ、GCログ、メトリクス、有効化しておいたJFR。何も変更しません。
- 軽量な読み取り:
VM.flags,GC.heap_info,VM.uptime。ここでヒープサイズの設定ミス(セクション5)を先に対象外とします。 - 短い一時停止:
Thread.printを間隔を空けて3回実行。 - 記録:
JFR.start duration=…. - 重い処理:ヒープダンプ、クラスヒストグラム。トラフィックを他のポッドに逃がすか(準備状態から外す)、
-XX:+HeapDumpOnOutOfMemoryErrorを使用してOOM発生時のみ自動的にダンプが取得されるように設定します。
そして、避けるべき事項です。
- 本番イメージやマニフェストにJDWPオプションを残さないようにします。特に
address=*:…とサービスの公開はリモートコード実行の経路となります。 - ヒープダンプおよびJFRファイルは機密情報を含むファイルとして取り扱います。外部に送信する場合、JFRは
scrubを適用し、ヒープダンプは原則として送信しません。 - コンテナのメモリ制限と
MaxRAMPercentageを合わせて設定します。デフォルトの25%では少なすぎ、90%ではヒープ外のメモリを圧迫します。 - 「CPU 9%」のような数値が何に対する割合なのかを確認します。10コアマシンの9%は、1つのコアを完全に使い切っていることを意味します。
11. まとめ
- デバッガー(JPDA)はJVM TI、JDWP、JDIの3つの層で構成されており、
-agentlib:jdwpがその入口を開きます。入口には認証がありません。実測では14バイトのハンドシェイクだけでVM情報を取得でき、デバッガーを切断してもポートは再び開放されました。Kubernetes環境では、ループバックとport-forwardを利用してAPIサーバーの認証の背後に配置します。 - 本番環境における一次ツールはjcmdです。同一コンテナ内でUnixドメインソケットを介して接続し、コマンドごとに負荷が異なります。クラスヒストグラムは実測においてFull GCを引き起こしました。
- 3つのケースに対する最初のアプローチ:停止には
Thread.printを3回、CPUにはtop -H→nid→ JFR、メモリには「Full GCの後でも減らないか」。 - JFRのデフォルト設定は実測でもオーバーヘッド1%未満であり、再起動なしで有効化できます。ファイルには環境変数が残るため、
jfr scrubを経由させてから出力します。
T-iの一言。 デバッガーでラリーを止めると、ボールも、相手も、観客もすべて止まってしまいます。試合中はjcmdで写真だけを撮り、ビデオ(JFR)は常時オンにしておくのがベストです。あ、それからビデオにロッカールームのパスワード(環境変数)が写り込んでしまうので、共有する前に必ず消去してくださいね!
参考
- JPDAアーキテクチャ https://docs.oracle.com/en/java/javase/21/docs/specs/jpda/architecture.html · JDWP接続・呼び出しオプション(JDK 21) https://docs.oracle.com/en/java/javase/21/docs/specs/jpda/conninv.html
- JDK 9リリースノート(JDWPのデフォルトバインディングがlocalhostになり、
*は「安全ではなく非推奨」、JDK-8041435) https://www.oracle.com/java/technologies/javase/9-notes.html - OpenJDK discussメーリングリスト(2025-01-20、JDWPのリモートコード実行の危険性への言及、公式ドキュメントではない) https://mail.openjdk.org/pipermail/discuss/2025-January/006502.html
- jcmd マニュアル JDK 21 https://docs.oracle.com/en/java/javase/21/docs/specs/man/jcmd.html · JDK 25 https://docs.oracle.com/en/java/javase/25/docs/specs/man/jcmd.html · JEP 444 (仮想スレッド、Thread.dump_to_file) https://openjdk.org/jeps/444
- java コマンド マニュアル JDK 21 (MaxRAMPercentage デフォルト 25%、HeapDumpOnOutOfMemoryError、StartFlightRecording) https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html · 8u92 リリースノート (ExitOnOutOfMemoryError) https://www.oracle.com/java/technologies/javase/8u92-relnotes.html
- JEP 158 統一 JVM ロギング https://openjdk.org/jeps/158 · JEP 271 統一 GC ロギング https://openjdk.org/jeps/271 · JEP 328 Flight Recorder https://openjdk.org/jeps/328
- jfr ツール マニュアル https://docs.oracle.com/en/java/javase/21/docs/specs/man/jfr.html · default.jfc (JDK 21u) https://raw.githubusercontent.com/openjdk/jdk21u/master/src/jdk.jfr/share/conf/jfr/default.jfc
- トラブルシューティングガイド: ハングとループ (デッドロックレポートの例、JDK 17) https://docs.oracle.com/en/java/javase/17/troubleshoot/troubleshoot-process-hangs-and-loops.html
- async-profiler https://github.com/async-profiler/async-profiler
- Keycloak コンテナ Dockerfile (UBI9-micro, java-21-openjdk-headless) https://github.com/keycloak/keycloak/blob/main/quarkus/container/Dockerfile
- Kubernetes port-forward https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/ · https://kubernetes.io/docs/reference/kubectl/generated/kubectl_port-forward/
- すべての原文は 2026年10月26日 にアクセスして確認しました。