- JAVA
スレッドダンプの取り方と読み方|jstack・jcmdとデッドロック
スレッドダンプは、Java プロセスの中にある全スレッドが「いま何をしているか」を写した一覧です。 取り方は jcmd <PID> Thread.print -l が基本で、JDK だけでなく JRE しかない環境では kill -3 <PID> も使えます。読むときは、状態(java.lang.Thread.State)・スタックの一番上・locked / waiting to lock の行の3点を見ます。デッドロックは、ダンプの末尾に Found one Java-level deadlock: と JVM が書き出してくれます。
| やりたいこと | コマンド | 出力先 |
|---|---|---|
| 普通に取る(推奨) | jcmd <PID> Thread.print -l |
コマンドの標準出力 |
| 普通に取る(従来) | jstack -l <PID> |
コマンドの標準出力 |
| JDK が無い環境で取る | kill -3 <PID> |
Java プロセス側の標準出力(ログファイル) |
| 仮想スレッドも含めて取る | jcmd <PID> Thread.dump_to_file -format=json <file> |
指定したファイル(JDK 21 以降) |
| CPU を食うスレッドを探す | top -H -p <PID> → ダンプの nid と照らす |
端末 |
jps -l # PID を調べる
jcmd 12345 Thread.print -l > td1.txt # 1回目
sleep 5
jcmd 12345 Thread.print -l > td2.txt # 2回目(比べるために複数回取る)
確認環境:OpenJDK 1.8.0_504・17.0.20.1・21.0.10・25.0.4.1(Ubuntu 24.04)で、デッドロック・CPU を使い続けるスレッド・ロック待ち・wait・ReentrantLock 待ち・キュー待ちを1つのプログラムで再現し、jstack・jcmd・kill -3・Thread.dump_to_file で取った出力を載せています。仮想スレッドは JDK 21・25 で1,000本立てて取りました。状態の定義は Java SE 21 の API ドキュメント(Thread.State)、jstack・仮想スレッドの扱いは Oracle の JDK ドキュメントで確かめています(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「スレッドダンプの取り方と読み方|jstack・jcmdとデッドロック」
スレッドダンプでわかること
スレッドダンプは、アプリが止まった・遅い・CPU が張り付いたときに、「どのスレッドが、どの行で、何を待っているか」を知るためのものです。ログに何も出ない種類の不具合に強いのが特徴です。
| 症状 | ダンプで見るところ | 見つかるもの |
|---|---|---|
| 画面が固まる・応答が返らない | BLOCKED のスレッドと、ロックを持っているスレッド |
デッドロック・長いロック保持 |
| CPU 使用率が高いまま | top -H で CPU を使うスレッド → 同じ nid の RUNNABLE |
無限ループ・重い処理の行 |
| リクエストが詰まる | 同じ行で止まっている WAITING が多数 |
接続プール待ち・外部 API 待ち |
| スレッド数が増え続ける | 同じ名前のスレッドの数 | スレッドの作りすぎ・プールの閉じ忘れ |
ヒープ(メモリ)の中身は入っていません。 メモリの調査はヒープダンプで行います。
取り方1:jcmd Thread.print(推奨)
jcmd は、動いている JVM に診断コマンドを送るツールです。Oracle の JDK 8 トラブルシューティングガイドでは、jstack よりも jcmd を使うことが勧められています(診断の機能が多く、性能への影響が小さいため)。
jcmd -l # Java プロセスの一覧(jps -l と同じ)
jcmd 12345 Thread.print -l > td.txt
jcmd <PID> help Thread.print で、JDK 21 のオプションを確かめた結果です。
Thread.print
Print all threads with stacktraces.
Impact: Medium: Depends on the number of threads.
Syntax : Thread.print [options]
Options: (options must be specified using the <key> or <key>=<value> syntax)
-l : [optional] print java.util.concurrent locks (BOOLEAN, false)
-e : [optional] print extended thread information (BOOLEAN, false)
-l を付けるのが大事です。 -l が無いと、ReentrantLock など java.util.concurrent のロックを誰が持っているか(Locked ownable synchronizers)が出ません。JDK 8 の jcmd には -l だけがあり、-e はありません。
出力は、先頭に PID の行(12345:)が付くほかは、jstack -l と同じ形でした(JDK 21 で2つを取り、時刻と経過時間以外が同じであることを diff で確かめました)。
取り方2:jstack
jstack -l 12345 > td.txt
JDK 21 の jstack のドキュメントには「This command is experimental and unsupported.」と書かれています。今も同梱されて動きますが、新しく手順を作るなら jcmd に寄せておくと安心です。
| 項目 | jcmd Thread.print | jstack |
|---|---|---|
| ロック情報 | -l で出る |
-l で出る |
| 位置づけ | 診断コマンドの1つ | ドキュメントでは「experimental and unsupported」 |
| 他の診断 | ヒープダンプ・JFR・GC 情報も同じコマンドで取れる | スレッドダンプだけ |
| 必要なもの | JDK(JRE には無い) | JDK(JRE には無い) |
どちらも、対象プロセスと同じユーザーで実行します。 別のユーザーや root から実行すると、JVM への接続(attach)に失敗することがあります。コンテナの中のプロセスには、そのコンテナの中で実行します。
取り方3:kill -3(JDK が無い環境)
kill -3(SIGQUIT)を送ると、JVM が自分でスレッドダンプを書き出します。プロセスは止まりません。
kill -3 12345
出力先は、コマンドを打った端末ではなく、Java プロセスの標準出力です。 Tomcat なら catalina.out、systemd で動かしていれば journalctl -u <サービス名> の中に出ます。JDK 8 で取ると、末尾にヒープの使用状況も付いていました。
Found 1 deadlock.
Heap
PSYoungGen total 28160K, used 4915K [0x00000000e0d80000, 0x00000000e2c80000, 0x0000000100000000)
eden space 24576K, 20% used [0x00000000e0d80000,0x00000000e124cf78,0x00000000e2580000)
ParOldGen total 64512K, used 0K [0x00000000a2800000, 0x00000000a6700000, 0x00000000e0d80000)
Metaspace used 4117K, capacity 4700K, committed 4864K, reserved 1056768K
kill -3 の出力には、-l を付けたときの Locked ownable synchronizers が出ませんでした(JDK 21)。ReentrantLock を誰が持っているかを見たいときは jcmd ... Thread.print -l を使います。Windows のコンソールで動かしているときは、Ctrl+Break で同じ出力が得られます(Atlassian のサポート文書による)。
取り方4:Thread.dump_to_file(JSON・仮想スレッド)
JDK 21 から、jcmd に Thread.dump_to_file が入りました。仮想スレッドも含めた全スレッドを、テキストまたは JSON でファイルに書き出します。
jcmd 12345 Thread.dump_to_file -format=json /tmp/td.json
jcmd 12345 Thread.dump_to_file /tmp/td.txt # 既定はテキスト
12345:
Created /tmp/td.json
同じ名前のファイルがあると、/tmp/td.json exists, use -overwrite to overwrite と出て書き出されません。-overwrite を付けると上書きします。パスは JVM プロセスから見たパスで、絶対パスで書くと迷いません。
JSON は JDK の版で中身が違いました。JDK 21 と JDK 25 で、デッドロック中の同じスレッドを取った結果です。
// JDK 21:名前とスタックだけ
{
"tid": "18",
"name": "worker-1",
"stack": [
"Demo.lambda$main$0(Demo.java:12)",
"java.base\/java.lang.Thread.run(Thread.java:1583)"
]
}
// JDK 25:状態・待っている相手・持っているロックが入る
{
"tid": "21",
"name": "worker-1",
"state": "BLOCKED",
"blockedOn": "java.lang.Object@42181d46",
"stack": [
"Demo.lambda$main$0(Demo.java:12)",
"java.base/java.lang.Thread.run(Thread.java:1474)"
],
"monitorsOwned": [
{ "depth": 0, "locks": [ "java.lang.Object@7b7f2426" ] }
]
}
JDK 25 では ReentrantLock 待ちのスレッドに "parkBlocker": { "object": "...ReentrantLock$NonfairSync@71d6861a", "owner": "27" } が付き、持ち主のスレッド ID まで JSON で追えました。 一方で、Found one Java-level deadlock の検出結果はどちらの版の Thread.dump_to_file にも出ません。デッドロックを探すときは Thread.print も取ります。
Oracle の仮想スレッドのドキュメントでは、この形式は「プラットフォームスレッドと仮想スレッドの両方を出すが、オブジェクトのアドレス・ロック・JNI の統計・ヒープの統計などは含まない」と説明されています(JDK 25 では上のとおりロックの一部が入るようになっています)。
4つの取り方の比較
| 取り方 | JDK 8 | JDK 17 | JDK 21 / 25 |
|---|---|---|---|
jstack -l |
○ | ○ | ○(ドキュメント上は experimental) |
jcmd Thread.print -l |
○ | ○ | ○(-e も使える) |
kill -3 |
○ | ○ | ○ |
jcmd Thread.dump_to_file |
×(コマンドが無い) | ×(コマンドが無い) | ○(JSON に状態が入るのは JDK 25) |
| 仮想スレッドが出るか | ― | ― | Thread.dump_to_file だけ |
スレッド1つ分の読み方
JDK 21 で取った、ロック待ちのスレッド1つ分です。
"lock-waiter" #22 [1205] prio=5 os_prio=0 cpu=0.50ms elapsed=7.15s tid=0x00007fb8e81291b0 nid=1205 waiting for monitor entry [0x00007fb876afe000]
java.lang.Thread.State: BLOCKED (on object monitor)
at Demo.lambda$main$3(Demo.java:20)
- waiting to lock <0x00000000a8519158> (a java.lang.Object)
at Demo$$Lambda/0x00007fb878001440.run(Unknown Source)
at java.lang.Thread.runWith(java.base@21.0.10/Thread.java:1596)
at java.lang.Thread.run(java.base@21.0.10/Thread.java:1583)
Locked ownable synchronizers:
- None
| 部分 | 例 | 意味 |
|---|---|---|
| スレッド名 | "lock-waiter" |
new Thread(..., "名前") やプールが付けた名前 |
#22 |
Java のスレッド ID | Thread.threadId() の値 |
[1205]・nid=1205 |
OS のスレッド ID | top -H の PID 列と同じ番号(JDK 21 以降は10進) |
daemon |
デーモンスレッドか | 付いていなければ、残っていると JVM が終わらない |
cpu=0.50ms |
そのスレッドが使った CPU 時間 | JDK 8 には無い項目 |
elapsed=7.15s |
スレッドができてからの時間 | JDK 8 には無い項目 |
java.lang.Thread.State |
Java から見た状態 | 下の表 |
at ... |
スタック。一番上が今いる場所 | 自分のコードの行を探す |
- locked <0x...> |
このスレッドが持っている synchronized のロック |
アドレスで相手を探す |
- waiting to lock <0x...> |
入ろうとして待っているロック | 同じアドレスを locked している人を探す |
Locked ownable synchronizers |
持っている ReentrantLock など |
-l を付けたときだけ出る |
状態の読み方表
状態の定義は Java SE 21 の API ドキュメント(Thread.State)によります。2列目は、ダンプに実際に出た表記です。
| 状態 | ダンプでの表記の例 | 原因になる呼び出し | 読み方 |
|---|---|---|---|
RUNNABLE |
RUNNABLE |
計算中・I/O 中 | CPU を使っているとは限らない。ソケットの読み込み待ちも RUNNABLE |
BLOCKED |
BLOCKED (on object monitor) |
synchronized に入れない |
誰かがロックを持ったまま。waiting to lock のアドレスを追う |
WAITING |
WAITING (on object monitor) |
Object.wait()・Thread.join() |
通知(notify)や終了を待っている |
WAITING |
WAITING (parking) |
LockSupport.park(ReentrantLock.lock・BlockingQueue.take など) |
parking to wait for の相手を追う |
TIMED_WAITING |
TIMED_WAITING (sleeping) |
Thread.sleep |
時間が来れば動く |
TIMED_WAITING |
TIMED_WAITING (parking) |
parkNanos(poll(timeout)・tryLock(timeout) など) |
時間つきの待ち。プールの待機スレッドに多い |
NEW・TERMINATED |
― | 開始前・終了後 | 通常のダンプには出ない |
ドキュメントには「これらは JVM の状態で、OS のスレッドの状態を表すものではない」とあります。RUNNABLE でも CPU を使っていないことがある(ネットワークの応答待ちなど)ので、CPU を使っているかは cpu= の増え方や top -H で確かめます。
よく見る待ち方の見分け
同じプログラムで再現した、待ち方ごとのスタックの先頭です(JDK 21)。
| スレッド | 状態 | スタック先頭の特徴 |
|---|---|---|
synchronized で待つ |
BLOCKED (on object monitor) |
- waiting to lock <0x00000000a8519158> |
wait() で待つ |
WAITING (on object monitor) |
Object.wait0 と - waiting on <...> |
ReentrantLock.lock() で待つ |
WAITING (parking) |
ReentrantLock$NonfairSync を parking to wait for |
BlockingQueue.take() で待つ |
WAITING (parking) |
LinkedBlockingQueue.take と ConditionObject |
sleep 中 |
TIMED_WAITING (sleeping) |
Thread.sleep0 |
ReentrantLock を持っているスレッドは、スタックに locked が出ず、-l の Locked ownable synchronizers にだけ出ます。
"rlock-holder" #24 [1207] prio=5 ... nid=1207 waiting on condition [0x00007fb8768fe000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep0(java.base@21.0.10/Native Method)
...
Locked ownable synchronizers:
- <0x00000000a8519240> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
待っている側の parking to wait for <0x00000000a8519240> と同じアドレスなので、この2つで「誰が誰を待たせているか」がつながります。
デッドロックを見つける
2つのスレッドが、互いに相手の持つロックを待つ状態を作りました。
static final Object LOCK_A = new Object();
static final Object LOCK_B = new Object();
new Thread(() -> { synchronized (LOCK_A) { sleep(100); synchronized (LOCK_B) { } } }, "worker-1").start();
new Thread(() -> { synchronized (LOCK_B) { sleep(100); synchronized (LOCK_A) { } } }, "worker-2").start();
ダンプの末尾に、JVM が見つけたデッドロックが出ます(JDK 21)。
Found one Java-level deadlock:
=============================
"worker-1":
waiting to lock monitor 0x00007fb84c069370 (object 0x00000000a8519148, a java.lang.Object),
which is held by "worker-2"
"worker-2":
waiting to lock monitor 0x00007fb8580015b0 (object 0x00000000a8519138, a java.lang.Object),
which is held by "worker-1"
Java stack information for the threads listed above:
===================================================
"worker-1":
at Demo.lambda$main$0(Demo.java:12)
- waiting to lock <0x00000000a8519148> (a java.lang.Object)
- locked <0x00000000a8519138> (a java.lang.Object)
...
Found 1 deadlock.
ReentrantLock のデッドロックも検出されます。 同じ形を ReentrantLock で作ると、waiting for ownable synchronizer という表記になりました。
Found one Java-level deadlock:
=============================
"tx-1":
waiting for ownable synchronizer 0x00000000a8517aa0, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "tx-2"
"tx-2":
waiting for ownable synchronizer 0x00000000a8517a70, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "tx-1"
Found one Java-level deadlock は JDK 8・17・21・25 のどれでも同じ形で出ました。検出は jstack・jcmd Thread.print・kill -3 のどれでも行われます。Thread.dump_to_file には出ません。
出ないときに疑うもの
JVM が検出するのは「ロックの持ち合い」だけです。次のような止まり方は Found ... deadlock に出ないので、スタックを自分で読みます。
| 止まり方 | ダンプでの見え方 | 例 |
|---|---|---|
| 長くロックを持つ | 1人が locked を持ったまま RUNNABLE / TIMED_WAITING、他は BLOCKED |
ロックの中で外部 API を呼ぶ |
| プールの枯渇 | 多数のスレッドが同じ行で WAITING (parking) |
DB 接続プールの取得待ち |
| 外部待ち | RUNNABLE のまま SocketInputStream.read などで止まる |
タイムアウトの無い HTTP 呼び出し |
| 自分のプールで自分を待つ | プールのスレッドが全員 Future.get で WAITING |
同じプールに子タスクを投げて待つ |
今回の再現では、lock-holder が sleep したままロックを持ち、lock-waiter と lock-waiter-2 の2本が同じアドレス <0x00000000a8519158> を waiting to lock していました。デッドロックの表示は出ません。同じアドレスを待つ BLOCKED が何本もあれば、その持ち主が「詰まりの元」です。
CPU 高騰のスレッドを特定する(top -H と nid)
CPU を使い続けるスレッド hot-loop を動かし、top -H(スレッドごとに表示)で見ました。
top -H -p 12345 # 対話で見る
top -H -b -n 1 -p 12345 # 1回だけ出力して終わる(記録向き)
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1203 root 25 5 4157064 42828 23568 R 90.0 0.5 0:17.27 hot-loop
1182 root 25 5 4157064 42828 23568 S 0.0 0.5 0:00.00 java
PID 列の 1203 が、OS から見たスレッドの番号です。Linux では top の COMMAND 列に Java のスレッド名(長い名前は途中まで)が出たので(JDK 17・21 で確認)、名前だけでも見当がつきました。番号をダンプの nid と照らします。
ここで JDK の版によって書き方が違います。 同じプログラムを各版で取った hot-loop の行です(JDK 11.0.32.1 でも、JDK 17 と同じく16進でした)。
JDK 8 : "hot-loop" #10 daemon prio=5 os_prio=0 tid=0x00007f0c8815c000 nid=0x9a1 runnable [0x00007f0c57afc000]
JDK 17: "hot-loop" #14 daemon prio=5 os_prio=0 cpu=4250.33ms elapsed=13.47s tid=0x00007f6cd410a2e0 nid=0x990 runnable [0x00007f6cb9afd000]
JDK 21: "hot-loop" #20 [1203] daemon prio=5 os_prio=0 cpu=6919.60ms elapsed=7.20s tid=0x00007fb8e8126bb0 nid=1203 runnable [0x00007fb876cfd000]
JDK 25: "hot-loop" #23 [2431] daemon prio=5 os_prio=0 cpu=4501.98ms elapsed=14.49s tid=0x00007fb4680eeea0 nid=2431 runnable [0x00007fb46c7c6000]
| JDK | nid の書き方 |
top -H の番号との照らし方 |
|---|---|---|
| 8 | 16進(nid=0x9a1) |
10進を16進にして探す |
| 11・17 | 16進(nid=0x990) |
10進を16進にして探す |
| 21・25 | 10進(nid=1203)、[1203] も付く |
そのままの数字で探す |
JDK 17 で top -H に出た 2448 を16進にすると 990 で、ダンプの nid=0x990 と一致しました。
printf '%x\n' 2448 # → 990
grep 'nid=0x990' td.txt # JDK 8・17
grep 'nid=1203 ' td.txt # JDK 21・25(後ろに空白を入れて別の番号との一致を避ける)
見つけたスレッドのスタックの一番上(今回は at Demo.spin(Demo.java:34))が、CPU を使っている場所です。1回だけだと「たまたまそこにいた」ことがあるので、次の章のように数秒おきに何回か取り、同じ場所にいるかを見ます。
top が使えない環境では、ps でも同じ番号が取れます。
ps -L -o tid,pcpu,comm -p 12345 | sort -k2 -nr | head
1203 95.8 hot-loop
1184 0.2 java
複数回取って比べる手順
1回のダンプは一瞬の写真です。数秒おきに3回以上取り、変わらないものを探すのが基本です。top -H も同時に残すと、CPU と照らせます。
#!/bin/sh
# 使い方: ./dumps.sh <PID> [回数] [間隔秒]
PID=$1; N=${2:-3}; INTERVAL=${3:-5}
for i in $(seq 1 "$N"); do
TS=$(date +%Y%m%d-%H%M%S)
jcmd "$PID" Thread.print -l > "threaddump-$PID-$TS.txt"
top -H -b -n 1 -p "$PID" > "top-$PID-$TS.txt"
sleep "$INTERVAL"
done
JDK 21 で5秒おきに3回取り、ロック待ちのスレッドの状態と、CPU を使うスレッドの cpu= を並べました。
for f in threaddump-*; do
echo "$f: $(grep -A1 '^"lock-waiter"' $f | tail -1 | xargs) / $(grep '^"hot-loop"' $f | grep -o 'cpu=[0-9.]*ms')"
done
threaddump-4270-20261013-144218.txt: java.lang.Thread.State: BLOCKED (on object monitor) / cpu=1539.01ms
threaddump-4270-20261013-144224.txt: java.lang.Thread.State: BLOCKED (on object monitor) / cpu=5592.72ms
threaddump-4270-20261013-144230.txt: java.lang.Thread.State: BLOCKED (on object monitor) / cpu=9556.79ms
| 比べるもの | 変わらないとき | 読み方 |
|---|---|---|
| 同じスレッドの状態とスタック | 3回とも同じ行で BLOCKED / WAITING |
止まっている(詰まり) |
RUNNABLE のスレッドのスタック |
3回とも同じメソッド | そこで時間を使っている |
cpu= の増え方(JDK 11 以降に出る) |
5秒で約4秒増える | ほぼ1コアを使い続けている |
| スレッドの数 | 回ごとに増える | スレッドの作りすぎ・漏れ |
| ロックの持ち主 | 同じスレッドが持ち続ける | ロックの中が遅い |
hot-loop は5秒ごとに cpu= が約4秒ずつ増えており、top -H の %CPU も3回とも 90.0 でした。
スレッドが多いときは、状態ごとの数を数えると見通しがよくなります。 上の3ファイルをまとめて数えた結果です。
grep -h 'java.lang.Thread.State' threaddump-*.txt | sort | uniq -c | sort -nr
27 java.lang.Thread.State: RUNNABLE
12 java.lang.Thread.State: BLOCKED (on object monitor)
9 java.lang.Thread.State: TIMED_WAITING (sleeping)
6 java.lang.Thread.State: WAITING (parking)
6 java.lang.Thread.State: WAITING (on object monitor)
3 java.lang.Thread.State: TIMED_WAITING (parking)
BLOCKED が1回あたり4本(デッドロックの2本と、ロック待ちの2本)あり、3回とも減っていません。回ごとに BLOCKED や WAITING (parking) が増えていくなら、詰まりが広がっていると読めます。
数が多いダンプは、GUI の解析ツール(JDK Mission Control・VisualVM や、オンラインの解析サービス)に読ませる方法もあります。オンラインのサービスに上げるときは、スレッド名やスタックに社内のクラス名・URL が含まれる点に気をつけます。
仮想スレッドのダンプ
仮想スレッド(JDK 21 から正式機能)を1,000本 sleep させた状態で取りました。
| 取り方 | 出たスレッド | 仮想スレッド |
|---|---|---|
jstack(JDK 21) |
21本 | 出ない(運んでいるキャリアスレッドだけ) |
jstack(JDK 25) |
22本 | 出ない |
Thread.dump_to_file(JDK 21) |
全部 | 1,000本+1本。テキスト 402,593 バイト、JSON 563,920 バイト |
Thread.dump_to_file(JDK 25) |
全部 | 同上。テキスト 629,647 バイト、JSON 889,930 バイト |
仮想スレッドを使うアプリでは、Thread.dump_to_file を使います。 JSON では、スレッドが「どの入れ物(コンテナ)に属するか」でまとめて出ます。JDK 21 の結果を数えると次のとおりでした。
('<root>', 8)
('java.util.concurrent.ThreadPoolExecutor@3a71f4dd', 0)
('ForkJoinPool-1/jdk.internal.vm.SharedThreadContainer@59e8d33d', 2)
('java.util.concurrent.ThreadPerTaskExecutor@32fcb886', 1000)
('ForkJoinPool.commonPool/jdk.internal.vm.SharedThreadContainer@3c1de7a4', 0)
('java.util.concurrent.ScheduledThreadPoolExecutor@36baf30c', 1)
Executors.newVirtualThreadPerTaskExecutor() で作った1,000本は ThreadPerTaskExecutor@... の下にまとまっています。どの Executor でスレッドが溜まっているかが一目でわかります。
ピン留めがダンプに出る(JDK 21)
synchronized の中で sleep する仮想スレッド vt-sync も入れておきました。JDK 21 の jstack では、キャリアスレッドに次の行が出ました。
"ForkJoinPool-1-worker-2" #67 [3319] daemon prio=5 os_prio=0 cpu=11.85ms elapsed=8.66s tid=0x00007fc0cc067fd0 [0x00007fc16c373000]
Carrying virtual thread #1020
at java.lang.VirtualThread.runContinuation(java.base@21.0.10/VirtualThread.java:245)
Carrying virtual thread は、仮想スレッドがキャリアに張り付いたまま(ピン留め)であることを示します。 同じプログラムを JDK 25 で動かすと、この行は出ませんでした。JDK 24 で synchronized の中でもキャリアを手放せるよう変わったためです(JEP 491)。JDK 25 の Thread.dump_to_file のテキストでは、仮想スレッドにも状態とロックが付いていました。
#1025 "vt-sync" virtual TIMED_WAITING 2026-10-13T05:42:01.925481094Z
at java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:787)
...
at Vt.lambda$main$1(Vt.java:9)
- locked <java.lang.Object@6286f6e6>
取るときの注意
| 注意点 | 内容 |
|---|---|
| 影響 | jcmd の説明では「Impact: Medium: Depends on the number of threads」。スレッドが多いほど時間がかかる |
| 実行ユーザー | 対象プロセスと同じユーザーで実行する |
| コンテナ | kubectl exec などでコンテナの中から実行する。JDK が無いイメージでは kill -3 を使い、ログから拾う |
| 中身 | スレッド名・クラス名・メソッド名が入る。変数の値は入らない |
| 保存先 | kill -3 はアプリのログに混ざる。ログのローテーションで消える前に取り出す |
よくある質問
Q. jstack を実行すると接続できないと言われます。
A. 多くは、実行するユーザーが Java プロセスと違うか、別のコンテナ・名前空間から実行しているためです。プロセスと同じユーザーで、同じコンテナの中から実行します。それでも取れないときは kill -3 でアプリのログに書き出させます。
Q. デッドロックの表示が出ないのに、画面が固まっています。 A. JVM が見つけるのはロックの持ち合いだけです。接続プールの取得待ちや、タイムアウトの無い外部呼び出しは表示されません。数秒おきに取り、同じ行で止まっているスレッドが多い場所を探します。同期の基本は関連記事「Java synchronizedで排他制御|スレッドセーフを実行結果で検証」で扱っています。
Q. RUNNABLE なのに CPU を使っていません。
A. ソケットの読み込み待ちなど、OS の中で待っている間も Java の状態は RUNNABLE です。CPU を使っているかは cpu= の増え方や top -H で見ます。
Q. スレッドプールのスレッドは、どれが自分のタスクかわかりません。
A. プールのスレッドに名前を付けると読みやすくなります。ThreadFactory で名前を付ける方法や仮想スレッドとの使い分けは、関連記事「Java ExecutorServiceの使い方|スレッドプールと仮想スレッド」で扱っています。
Q. メモリ不足の調査にもスレッドダンプは使えますか。 A. スレッドダンプにはオブジェクトの中身が入らないため、メモリの調査には向きません。ヒープダンプの取り方と解析は関連記事「ヒープダンプの取り方と解析|OutOfMemoryErrorの原因を探す」で、メッセージごとの原因は関連記事「Java OutOfMemoryErrorの原因と対処|メッセージ別に解説」で扱っています。時間を通した CPU やロックの記録は関連記事「JFR(Java Flight Recorder)入門|記録とjfrコマンド分析」で扱っています。
まとめ
- 取り方の基本は
jcmd <PID> Thread.print -l。-lでReentrantLockの持ち主まで出る - JDK が無い環境は
kill -3。出力は Java プロセスの標準出力(ログ)に出る - 読むのは 状態・スタックの一番上・
locked/waiting to lockのアドレス - デッドロックは末尾の
Found one Java-level deadlock。ReentrantLockも検出される - CPU 高騰は
top -Hの番号とダンプのnidを照らす。JDK 8・17 は16進、21 以降は10進 - 数秒おきに3回以上取り、変わらないスレッドを探す
- 仮想スレッドは
jstackに出ない。Thread.dump_to_file -format=jsonを使う(JSON に状態が入るのは JDK 25)
参考・出典
確認日はいずれも 2026年10月13日です。
- Java SE 21 API「Thread.State」:https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.State.html
- JDK 21 Tool Specifications「jstack」:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jstack.html
- JDK 21 Tool Specifications「jcmd」:https://docs.oracle.com/en/java/javase/21/docs/specs/man/jcmd.html
- Java Platform, Standard Edition Core Libraries 21「Virtual Threads」:https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html
- Java SE 8 Troubleshooting Guide「The jcmd Utility」:https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr016.html
- OpenJDK「JEP 491: Synchronize Virtual Threads without Pinning」:https://openjdk.org/jeps/491
- Atlassian「Generating a Thread Dump Externally」:https://ja.confluence.atlassian.com/spaces/CONF58/pages/771893160/Generating+a+Thread+Dump+Externally
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「スレッドダンプの取り方と読み方|jstack・jcmdとデッドロック」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。