スレッドダンプは、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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?