jdb(Java Debugger)は、JDKに付属するJavaのコマンドラインデバッガです。ブレークポイントで止めて変数を見る、1行ずつ進める、稼働中のJVMに外から接続する、といった操作をすべてコマンドで行います。GUIの無いサーバーやコンテナの中でも、端末さえあれば使えます。
この記事では、Temurin 25.0.4.1(2026年10月時点の最新LTSであるJDK 25系)でjdbを実際に動かし、その出力をそのまま載せています。なお「JDB」はラオスの銀行Joint Development Bank(JDB銀行)や、音楽アーティストのJ.D.Bを指すこともありますが、ここで扱うのはJavaのデバッガです。
まとめ:jdbの要点
- jdbはJDKの
binに入っており、追加のインストールは要りません。JDK 9以降はjdk.jdiモジュールに含まれます。 - ローカル変数を見るには
javac -gでコンパイルします。付けないとLocal variable information not availableになります。 - 基本は
stop at クラス:行→run→locals/print/where→next/step/contの順です。 - 稼働中のJVMには、対象を
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005で起動し、jdb -attach 5005で接続します。 - JDK 9以降、
address=5005のようにホストを省くとループバックアドレスだけで待ち受けます(実測では127.0.0.1)。Dockerの-p転送で繋ぐにはaddress=*:5005が要りました。 - JDWPには認証がありません。全インターフェースで開けたまま本番で常駐させるのは避けます。
- ブレークポイントなどのイベントが届いていない仮想スレッドは、既定の
threadsに出ません。jdb -trackallthreadsで起動すると表示されます。
jdbはJDKに付属するコマンドラインデバッガ
jdbは、JDKのデバッグ基盤であるJPDA(Java Platform Debugger Architecture)の上で動く参照実装のデバッガです。jdk.jdiモジュールの公式説明にも、このモジュールに簡単なコマンドラインデバッガjdbが含まれると書かれています。デバッガ側はJDI(Java Debug Interface)、対象のJVM側はJDWP(Java Debug Wire Protocol)エージェントを使い、両者がソケットなどで通信します。IntelliJ IDEAやVS CodeのJavaデバッガも対象JVMとはJDWPで話すので、jdbで覚えた接続方法はIDEでもそのまま通用します。
jdbはJDKの bin にあり、JREには入っていません。eclipse-temurin:25-jre イメージで確かめると、bin にjdbが無く、jdk.jdi モジュールもありませんでした。一方、対象側のJDWPエージェント(jdk.jdwp.agent モジュール)はJREイメージにも入っていて、-agentlib:jdwp で待ち受けを始められます。JREで動くアプリケーションは、jdbをJDKのある手元から接続して調べることになります。手元のJDKにjdbがあるかは次のように確かめます(既定は日本語表示なので、英語にしたいときは -J-Duser.language=en を付けます)。
$ $JAVA_HOME/bin/jdb -J-Duser.language=en -version
This is jdb version 25.0 (Java SE version 25.0.4.1)
2026年9月15日にGAとなったJDK 27でも This is jdb version 27.0 (Java SE version 27) と表示され、-help の内容はJDK 25と一字一句同じでした。JDKの入れ替えにはSDKMAN!の使い方|Javaのインストール・切り替えと.sdkmanrc、Windows・macOSの注意点が使えます。JDK 27で何が変わったかはJava 27とは|非LTSの半年運用とG1・オブジェクトヘッダ既定化の影響にまとめています。
jdbの起動とブレークポイントでの停止
javac -gでのコンパイルとjdbからの起動
次のプログラムを例にします。ループで合計を出したあと、わざと0で割ってArithmeticExceptionを起こします。
public class Demo {
static int counter = 0;
static int divide(int a, int b) {
return a / b;
}
public static void main(String[] args) throws Exception {
int total = 0;
for (int i = 1; i <= 3; i++) {
total += i * 10;
counter++;
}
System.out.println("total=" + total);
System.out.println(divide(total, counter - 3));
}
}
javac -g でローカル変数の情報を含めてコンパイルし、java の代わりに jdb でクラスを指定します。jdbはまだJVMを起動していないので、先にブレークポイントを置いてから run します。
$ javac -g Demo.java
$ jdb -classpath . Demo
Initializing jdb ...
> stop at Demo:11
Deferring breakpoint Demo:11.
It will be set after the class is loaded.
> run
run Demo
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Demo:11
Breakpoint hit: "thread=main", Demo.main(), line=11 bci=9
11 total += i * 10;
main[1]
プロンプトが main[1] に変わったら、mainスレッドが11行目で止まっています。run を打った時点で Set uncaught java.lang.Throwable と表示されるとおり、jdbは捕捉されない例外が起きたときにも自動で止まります。
変数とスタックの表示(locals・print・where)
main[1] locals
Method arguments:
args = instance of java.lang.String[0] (id=435)
Local variables:
total = 0
i = 1
main[1] print total
total = 0
main[1] where
[1] Demo.main (Demo.java:11)
staticフィールドは print Demo.counter のようにクラス名を付ければ読めます。一方 dump Demo とクラス名だけを渡すと ParseException: Name unknown: Demo になりました。dump はオブジェクトを渡すコマンドなので、クラスの構造を見たいときは fields Demo や methods Demo を使います。
ステップ実行と値の書き換え(next・step・step up・set)
next は呼び出し先に入らず次の行へ、step は呼び出し先のメソッドの中へ入り、step up は今のメソッドを呼び出し元へ戻るまで実行します。止まっている間は set で値を書き換えられます。次は stop in Demo.divide でdivideの先頭に止め、0になっている引数bを1に書き換えた例です。
Breakpoint hit: "thread=main", Demo.divide(), line=5 bci=0
5 return a / b;
main[1] set b = 1
b = 1 = 1
main[1] step up
Step completed: "thread=main", Demo.main(), line=15 bci=54
main[1] cont
60
The application exited
本来は0除算で落ちるところが、60を出力して正常に終わりました。修正案が正しいかを、コードを直して再ビルドする前にその場で試せます。
例外とフィールド変更での停止(catch・watch)
catch java.lang.ArithmeticException を設定すると、その例外が投げられた行で止まります。watch Demo.counter はフィールドが変更されるたびに止まり、変更前と変更後の値を表示します。
main[1] catch java.lang.ArithmeticException
Set all java.lang.ArithmeticException
main[1] cont
total=60
Exception occurred: java.lang.ArithmeticException (uncaught)"thread=main", Demo.divide(), line=5 bci=2
5 return a / b;
> watch Demo.counter
Deferring watch modification of Demo.counter.
(中略)
Field (Demo.counter) is 0, will be 1: "thread=main", Demo.main(), line=12 bci=21
12 counter++;
watch は既定で変更だけを監視し、読み取りも止めたいときは watch access、両方なら watch all を指定します。どこで値が書き換わったか分からない不具合は、ブレークポイントを何か所も置くよりこちらのほうが早く見つかります。
jdbのコマンド一覧と起動オプション
プロンプトで help と打つと全コマンドが出ます。よく使うものを役割ごとに並べました(JDK 25の help の出力から抜粋)。
| コマンド | 役割 |
|---|---|
run [クラス [引数]] |
mainクラスの実行を開始 |
stop at クラス:行 / stop in クラス.メソッド |
ブレークポイントを設定 |
clear クラス:行 / clear |
解除 / 一覧表示 |
cont |
止まった位置から再開 |
next / step / step up |
次の行 / 中へ入る / 呼び出し元へ戻る |
locals / print 式 / dump 式 |
ローカル変数 / 式の値 / オブジェクト全体 |
set 変数 = 式 |
変数・フィールドの値を書き換え |
where [all] / up / down |
スタック表示 / フレーム移動 |
threads / thread ID |
スレッド一覧 / 対象スレッドの切り替え |
catch 例外クラス |
例外の発生で停止 |
watch [access|all] クラス.フィールド |
フィールドの変更・参照で停止 |
list / use パス |
ソース表示 / ソース検索パスの変更 |
redefine クラス ファイル |
クラスを再定義 |
exit / quit |
jdbを終了 |
毎回打つコマンドは、ホームディレクトリかカレントディレクトリの .jdbrc(または jdb.ini)に書いておくと起動時に実行されます。read ファイル名 でも同じ形式のファイルを読み込めるので、ブレークポイントの組み合わせを手順書として残せます。
起動オプションはJDK 25のjdbマニュアルに載っています。実務で使うのは次の7つです。
| オプション | 意味 |
|---|---|
-classpath パス |
jdbが起動するJVMのクラスパス |
-sourcepath パス |
ソースの検索先(省略時は .) |
-attach アドレス |
待ち受け中のJVMへ接続 |
-listen アドレス |
JVMからの接続を待つ |
-launch |
run を待たずにすぐ起動 |
-trackallthreads |
仮想スレッドを含む全スレッドを追跡 |
-J / -R |
jdb自身 / 対象JVMへオプションを渡す |
稼働中のJVMへのjdb接続(リモートデバッグ)
-agentlib:jdwpによる対象JVMの起動
サーバーで動いているアプリケーションを調べるときは、jdbから起動するのではなく、対象のJVMにJDWPエージェントを読み込ませて起動しておき、あとから接続します。サブオプションの意味はJPDAのConnection and Invocation Detailsに定義されています。
| サブオプション | 既定値 | 意味 |
|---|---|---|
transport |
なし(必須) | 通信方式。通常は dt_socket |
server |
n |
y でデバッガの接続を待つ |
address |
なし | 待ち受け・接続先のホストとポート |
suspend |
y |
y は接続までmainの前で停止 |
allow |
* |
接続を受け付ける送信元アドレス |
includevirtualthreads |
n |
スレッド一覧に仮想スレッドを含める |
例として、0.5秒ごとに work を呼ぶだけのクラスを -g 付きでコンパイルし、エージェントを付けて起動します。
public class Loop {
public static void main(String[] args) throws Exception {
for (int n = 0; n < 30; n++) {
work(n);
Thread.sleep(500);
}
}
static void work(int n) {
String msg = "tick " + n;
System.out.println(msg);
}
}
$ javac -g Loop.java
$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -cp . Loop
Listening for transport dt_socket at address: 5005
tick 0
tick 1
suspend の既定は y なので、書き忘れるとアプリケーションはデバッガが繋がるまで1行も実行されません。実測でも suspend=y で起動したJVMは、接続するまで最初の tick 0 すら出力しませんでした。起動直後の初期化処理を調べたいときだけ y にし、サーバーを調べるときは n にします。
jdb -attachでの接続と停止
$ jdb -attach 5005
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
> stop in Loop.work
Set breakpoint Loop.work
>
Breakpoint hit: "thread=main", Loop.work(), line=9 bci=0
9 String msg = "tick " + n;
main[1] print n
n = 8
main[1] next
Step completed: "thread=main", Loop.work(), line=10 bci=7
main[1] print msg
msg = "tick 8"
別ホストなら jdb -attach ホスト名:5005 とします。-attach で接続した場合、jdbを exit しても対象のJVMは止まらず、そのまま動き続けました。本番に近い環境で調べ終えたら、ブレークポイントを clear クラス:行 で解除し、cont で再開してから抜けると、スレッドを止めたまま離れる事故を防げます。
-listenとJAVA_TOOL_OPTIONSによる逆向き接続
起動コマンドを書き換えにくいアプリケーションには、環境変数 JAVA_TOOL_OPTIONS でエージェントを渡せます。次の例はjdb側が待ち受け(-listen)、対象のJVMが server=n で接続しにいく逆向きの構成です。
$ jdb -listen 5005
Listening at address: localhost:5005
# 別の端末で
$ JAVA_TOOL_OPTIONS='-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=localhost:5005' java -cp . Demo
Picked up JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=localhost:5005
対象のJVMは起動時に Picked up JAVA_TOOL_OPTIONS と標準エラーに出すので、環境変数が効いたかはログで確かめられます。短命なバッチやテストのように、起動してすぐ終わるプロセスを捕まえるときは、この逆向き接続のほうが確実です。
Dockerコンテナ・Kubernetes上のアプリケーションへの接続
コンテナでは待ち受けアドレスに注意が要ります。docker run -p 127.0.0.1:5005:5005 でポートを転送し、コンテナ内のJVMを address=5005 で起動したところ、ホストのjdbは java.io.IOException: handshake failed - connection prematurally closed で接続できませんでした。address=*:5005 に変えると接続でき、threads でスレッド一覧まで取れました。ホスト側の公開先を 127.0.0.1 に限定すれば、ホストの外のネットワークからは繋がりません。ただし *:5005 で待ち受けているので、同じDockerネットワークにいる別のコンテナからは接続できます。調査が終わったらコンテナを起動し直してエージェントを外します。
$ docker run --rm -p 127.0.0.1:5005:5005 -v "$PWD":/app -w /app eclipse-temurin:25-jdk \
java '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005' -cp . Loop
$ jdb -attach localhost:5005
Kubernetesでは、Serviceでデバッグ用ポートを公開せず、kubectl port-forwardで手元のポートへ転送してから jdb -attach localhost:5005 とします。zshでは *:5005 がワイルドカードとして展開されて no matches found になるので、オプション全体を引用符で囲んでください。
JDK 9以降のバインド先とJDWPの危険性
JDK 9で、address にホストを書かない場合の待ち受け先がlocalhostだけに変わりました。JDK 9のリリースノート(JDK-8041435)は、全インターフェースで待ち受ける旧来の動作には * を使えるが、安全ではなく推奨しないと書いています。JDK 25で lsof を取ると、違いがそのまま見えます。
address=5005 → TCP 127.0.0.1:5005 (LISTEN)
address=*:5005 → TCP *:5005 (LISTEN)
古い記事に多い address=5005 の例が、別ホストから繋がらなくなった原因はこれです。逆に * を付けると、そのポートに届く相手なら誰でも接続できます。JDWPには認証が無く、JDWPを検出するTenableのNessusプラグイン58400も、認証なしで接続した攻撃者が任意のJavaコードを実行できると説明しています。
allow で送信元を絞ることはできます。allow=192.168.1.0/24 を付けたJVMにlocalhostから接続すると、JVM側に Debugger failed to attach: ERROR: Peer not allowed to connect が出て拒否されました。ただしこれは送信元アドレスによる制限で、認証ではありません。判断の基準は次のとおりです。
- 本番のJVMに
*:ポートのJDWPを常時付けておかない。調査のたびに付けて再起動し、終わったら外す。 - 別ホストから調べるときは、
address=5005(localhostのみ)のままSSHのポート転送やkubectl port-forwardで繋ぐ。 - どうしても
*で開くなら、allowとファイアウォールの両方で送信元を絞る。
jdbでの仮想スレッドの追跡(-trackallthreads)
JDK 21で正式化された仮想スレッド(JEP 444)は、jdbの既定では一覧に出ないことがあります。jdbのマニュアルによると、既定ではイベント(ブレークポイントなど)が届いた仮想スレッドだけを追跡し、-trackallthreads を付けると生成時から全スレッドを追跡します。
次のクラスで仮想スレッドを3本起動して Thread.sleep させ、javac -g VT2.java でコンパイルしたうえで、mainスレッドの13行目(System.out.println("started");)に stop at VT2:13 を置いて threads を打つと、結果はこう分かれました。
import java.util.ArrayList;
import java.util.List;
public class VT2 {
public static void main(String[] args) throws Exception {
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
list.add(Thread.ofVirtual().name("vt-" + i).start(() -> {
try { Thread.sleep(5000); } catch (InterruptedException e) { }
}));
}
Thread.sleep(500);
System.out.println("started");
for (Thread t : list) t.join();
}
}
# jdb -classpath . VT2 (既定)
Group CarrierThreads:
(jdk.internal.misc.CarrierThread)689 ForkJoinPool-1-worker-1 cond. waiting
(VirtualThreadsグループは表示されない)
# jdb -trackallthreads -classpath . VT2
Group VirtualThreads:
(java.lang.VirtualThread)707 vt-0 cond. waiting
(java.lang.VirtualThread)710 vt-1 cond. waiting
(java.lang.VirtualThread)712 vt-2 cond. waiting
ブレークポイントに当たった仮想スレッドは既定でも VirtualThreads グループに running (at breakpoint) と表示され、where では java.lang.VirtualThread.run と jdk.internal.vm.Continuation.enter がスタックの下に積まれていました。止まっていない仮想スレッドまで見たいときだけ -trackallthreads を付けます。対象のJVM側には、デバッガが求めるスレッド一覧に仮想スレッドを含める includevirtualthreads という別のオプションがあります。jdbのマニュアルによると、-trackallthreads はjdbがアプリケーションを起動する場合にこのオプションを自動で有効にします。-attach で接続する場合は、対象のJVMを includevirtualthreads=y 付きで起動しておきます。それでも接続前に生成された仮想スレッドは含まれない場合があり、数が非常に多いとデバッガが処理しきれなくなるとJPDAのドキュメントは注意しています。仮想スレッドそのものの仕組みはJava 仮想スレッド(Virtual Thread)とProject Loom入門|作り方・仕組み・Spring Boot設定をJDK 25で実測で扱っています。
jdbがうまく動かないときの原因と対処
実測で出たエラーと、その原因です。
Local variable information not available. Compile with -g:-gを付けずにコンパイルしたクラスです。localsが使えず、print totalもName unknownになります。Mavenならmaven-compiler-pluginのdebug設定(既定はtrue)を確かめます。- ブレークポイントが当たらず最後まで走る:
stop at Dem:11のようにクラス名を誤ると、Deferring breakpoint Dem:11.のまま一度も当たらず、エラーも出ません。パッケージ付きの完全修飾名(com.example.Demo:11)で書き、clearで一覧を確かめます。 java.net.ConnectException: Connection refusedとUnable to attach to target VM.:そのポートで誰も待ち受けていません。対象のJVMの起動ログにListening for transport dt_socket at address:が出ているか、ポート番号が合っているかを見ます。handshake failed - connection prematurally closed:今回の検証では、コンテナ内のJVMがlocalhostだけで待ち受けていたときにこのエラーになり、address=*:5005にすると接続できました。- 止まった行のソースが表示されない:jdbのソース検索先は既定でカレントディレクトリです。
-sourcepath src/main/javaで起動するか、プロンプトでuseにパスを渡して変更します(引数なしのuseは現在の検索パスを表示するだけです)。
不具合を再現・切り分けする手順そのものはデバッグとは?バグを再現・切り分け・修正する体系的な手順と手法を実装者向けに解説にまとめています。
jdbとIDE・jcmd・jhsdbの使い分け
手元のマシンでGUIが使えるなら、変数の展開やブレークポイントの操作はIDEのデバッガで行うほうが手数が少なく済みます。jdbを選ぶのは、GUIが無いサーバーやコンテナの中で調べる場面と、.jdbrc や read で同じ操作を繰り返したい場面です。止めずにスレッドの状態を見たいだけなら、デバッガを繋ぐ必要もありません。
| ツール | 向く場面 | 接続の仕組み |
|---|---|---|
| IDEのデバッガ | 手元の開発・テスト | JDWP |
| jdb | GUIの無いサーバー・コンテナ | JDWP |
jcmd PID Thread.print |
稼働中のスタックを一度だけ取る | Attach API(JDWP不要) |
jhsdb clhsdb |
コアダンプの解析 | Serviceability Agent |
jcmdのマニュアルによると、Thread.print はプラットフォームスレッドとマウント中の仮想スレッドのスタックを表示し、全スレッドを書き出したいときは Thread.dump_to_file を使います。jhsdbはJavaプロセスへの接続のほか、クラッシュ後のコアダンプの解析に使えます。ただしマニュアルは、jhsdbを実験的でサポート対象外のツールとしたうえで、稼働中のプロセスに接続するとプロセスが停止し、切り離すときにクラッシュするおそれがあると注意しています。止められないプロセスには使わず、コアダンプの解析に用途を絞ります。jcmdでネイティブメモリを調べる手順はNative Memory Tracking(NMT)とは?jcmdでJVMのネイティブメモリを調べる手順、JVMのスレッドやメモリの構成はJava仮想マシン(JVM)とは?仕組み・メモリ構成・JITを実機の出力で解説で扱っています。Javaで業務システムを一から作る相談はフルスクラッチ開発で受け付けています。
よくある質問
JDBは何の略ですか?
Javaの文脈ではJava Debuggerの略で、JDKに付属するコマンドラインデバッガjdbを指します。ほかに、ラオスの銀行Joint Development Bank(公式サイトの表記はJoint Development Bank Public)の略称として使われ、1999年に統合された旧日本開発銀行(Japan Development Bank)の略称としても使われていました。
JDB銀行とjdbは関係がありますか?
関係はありません。JDB銀行はラオスの銀行で、公式サイトの英語名はJoint Development Bank Publicです。
jdbを使うのにインストールは必要ですか?
JDKがあれば不要で、$JAVA_HOME/bin/jdb にあります。JREには含まれないので、JREで動くアプリケーションはJDKのある手元のマシンから接続します。
jdbでリモートデバッグするにはどうすればよいですか?
対象のJVMを -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 で起動し、jdb -attach ホスト名:5005 で接続します。JDK 9以降はホストを省くとループバックアドレスでしか待ち受けないので、別ホストからはSSHのポート転送で繋ぎます。
jdbとIntelliJ IDEAなどIDEのデバッガは何が違いますか?
どちらも対象のJVMとはJDWPで通信するので、土台は同じです。IDEは変数の木構造表示やソース上のブレークポイント操作が得意で、jdbはGUIが無い環境で動き、操作を .jdbrc に書いて再現できます。