什么是JDWP ?
JDWP 是 Java Debug Wire Protocol 的縮寫(xiě),它定義了調(diào)試器(debugger)和被調(diào)試的 Java 虛擬機(jī)(target vm)之間的通信協(xié)議。
JDWP 協(xié)議介紹
這里首先要說(shuō)明一下 debugger 和 target vm。Target vm 中運(yùn)行著我們希望要調(diào)試的程序,它與一般運(yùn)行的 Java 虛擬機(jī)沒(méi)有什么區(qū)別,只是在啟動(dòng)時(shí)加載了 Agent JDWP 從而具備了調(diào)試功能。而 debugger 就是我們熟知的調(diào)試器,它向運(yùn)行中的 target vm 發(fā)送命令來(lái)獲取 target vm 運(yùn)行時(shí)的狀態(tài)和控制 Java 程序的執(zhí)行。Debugger 和 target vm 分別在各自的進(jìn)程中運(yùn)行,他們之間的通信協(xié)議就是 JDWP。
JDWP 與其他許多協(xié)議不同,它僅僅定義了數(shù)據(jù)傳輸?shù)母袷?,但并沒(méi)有指定具體的傳輸方式。這就意味著一個(gè) JDWP 的實(shí)現(xiàn)可以不需要做任何修改就正常工作在不同的傳輸方式上(在 JDWP 傳輸接口中會(huì)做詳細(xì)介紹)。
JDWP 是語(yǔ)言無(wú)關(guān)的。理論上我們可以選用任意語(yǔ)言實(shí)現(xiàn) JDWP。然而我們注意到,在 JDWP 的兩端分別是 target vm 和 debugger。Target vm 端,JDWP 模塊必須以 Agent library 的形式在 Java 虛擬機(jī)啟動(dòng)時(shí)加載,并且它必須通過(guò) Java 虛擬機(jī)提供的 JVMTI 接口實(shí)現(xiàn)各種 debug 的功能,所以必須使用 C/C++ 語(yǔ)言編寫(xiě)。而 debugger 端就沒(méi)有這樣的限制,可以使用任意語(yǔ)言編寫(xiě),只要遵守 JDWP 規(guī)范即可。JDI(Java Debug Interface)就包含了一個(gè) Java 的 JDWP debugger 端的實(shí)現(xiàn)(JDI 將在該系列的下一篇文章中介紹),JDK 中調(diào)試工具 jdb 也是使用 JDI 完成其調(diào)試功能的。
圖 1. JDWP agent 在調(diào)試中扮演的角色

協(xié)議分析
JDWP 大致分為兩個(gè)階段:握手和應(yīng)答。握手是在傳輸層連接建立完成后,做的第一件事:
Debugger 發(fā)送 14 bytes 的字符串“JDWP-Handshake”到 target Java 虛擬機(jī)
Target Java 虛擬機(jī)回復(fù)“JDWP-Handshake”
圖 2. JDWP 的握手協(xié)議

握手完成,debugger 就可以向 target Java 虛擬機(jī)發(fā)送命令了。JDWP 是通過(guò)命令(command)和回復(fù)(reply)進(jìn)行通信的,這與 HTTP 有些相似。JDWP 本身是無(wú)狀態(tài)的,因此對(duì) command 出現(xiàn)的順序并不受限制。JDWP 有兩種基本的包(packet)類型:命令包(command packet)和回復(fù)包(reply packet)。
Debugger 和 target Java 虛擬機(jī)都有可能發(fā)送 command packet。Debugger 通過(guò)發(fā)送 command packet 獲取 target Java 虛擬機(jī)的信息以及控制程序的執(zhí)行。Target Java 虛擬機(jī)通過(guò)發(fā)送 command packet 通知 debugger 某些事件的發(fā)生,如到達(dá)斷點(diǎn)或是產(chǎn)生異常。
Reply packet 是用來(lái)回復(fù) command packet 該命令是否執(zhí)行成功,如果成功 reply packet 還有可能包含 command packet 請(qǐng)求的數(shù)據(jù),比如當(dāng)前的線程信息或者變量的值。從 target Java 虛擬機(jī)發(fā)送的事件消息是不需要回復(fù)的。還有一點(diǎn)需要注意的是,JDWP 是異步的:command packet 的發(fā)送方不需要等待接收到 reply packet 就可以繼續(xù)發(fā)送下一個(gè) command packet。
Packet 的結(jié)構(gòu)
Packet 分為包頭(header)和數(shù)據(jù)(data)兩部分組成。包頭部分的結(jié)構(gòu)和長(zhǎng)度是固定,而數(shù)據(jù)部分的長(zhǎng)度是可變的,具體內(nèi)容視 packet 的內(nèi)容而定。Command packet 和 reply packet 的包頭長(zhǎng)度相同,都是 11 個(gè) bytes,這樣更有利于傳輸層的抽象和實(shí)現(xiàn)。
- Command packet 的 header 的結(jié)構(gòu) :
-
圖 3. JDWP command packet 結(jié)構(gòu)
JDWP command packet 結(jié)構(gòu)
Length 是整個(gè) packet 的長(zhǎng)度,包括 length 部分。因?yàn)榘^的長(zhǎng)度是固定的 11 bytes,所以如果一個(gè) command packet 沒(méi)有數(shù)據(jù)部分,則 length 的值就是 11。
Id 是一個(gè)唯一值,用來(lái)標(biāo)記和識(shí)別 reply 所屬的 command。Reply packet 與它所回復(fù)的 command packet 具有相同的 Id,異步的消息就是通過(guò) Id 來(lái)配對(duì)識(shí)別的。
Flags 目前對(duì)于 command packet 值始終是 0。
Command Set 相當(dāng)于一個(gè) command 的分組,一些功能相近的 command 被分在同一個(gè) Command Set 中。Command Set 的值被劃分為 3 個(gè)部分:
0-63: 從 debugger 發(fā)往 target Java 虛擬機(jī)的命令
64 – 127: 從 target Java 虛擬機(jī)發(fā)往 debugger 的命令
128 – 256: 預(yù)留的自定義和擴(kuò)展命令
- Reply packet 的 header 的結(jié)構(gòu):
-
圖 4. JDWP reply packet 結(jié)構(gòu)
JDWP reply packet 結(jié)構(gòu)
Length、Id 作用與 command packet 中的一樣。
Flags 目前對(duì)于 reply packet 值始終是 0x80。我們可以通過(guò) Flags 的值來(lái)判斷接收到的 packet 是 command 還是 reply。
Error Code 用來(lái)表示被回復(fù)的命令是否被正確執(zhí)行了。零表示正確,非零表示執(zhí)行錯(cuò)誤。
Data 的內(nèi)容和結(jié)構(gòu)依據(jù)不同的 command 和 reply 都有所不同。比如請(qǐng)求一個(gè)對(duì)象成員變量值的 command,它的 data 中就包含該對(duì)象的 id 和成員變量的 id。而 reply 中則包含該成員變量的值。
JDWP 還定義了一些數(shù)據(jù)類型專門(mén)用來(lái)傳遞 Java 相關(guān)的數(shù)據(jù)信息。下面列舉了一些數(shù)據(jù)類型,詳細(xì)的說(shuō)明參見(jiàn) [1]
- 表 1. JDWP 中數(shù)據(jù)類型介紹
| 名稱 | 長(zhǎng)度 | 說(shuō)明 |
|---|---|---|
| byte | 1 byte | byte 值。 |
| boolean | 1 byte | 布爾值,0 表示假,非零表示真。 |
| int | 4 byte | 4 字節(jié)有符號(hào)整數(shù)。 |
| long | 8 byte | 8 字節(jié)有符號(hào)整數(shù)。 |
| objectID | 依據(jù) target Java 虛擬機(jī)而定,最大 8 byte | Target Java 虛擬機(jī)中對(duì)象(object)的唯一 ID。這個(gè)值在整個(gè) JDWP 的會(huì)話中不會(huì)被重用,始終指向同一個(gè)對(duì)象,即使該對(duì)象已經(jīng)被 GC 回收(引用被回收的對(duì)象將返回 INVALID_OBJECT 錯(cuò)誤。 |
| Tagged-objectID | objectID 的長(zhǎng)度加 1 | 第一個(gè) byte 表示對(duì)象的類型,比如,整型,字符串,類等等。緊接著是一個(gè) objectID。 |
| threadID | 同 objectID 的長(zhǎng)度 | 表示 Target Java 虛擬機(jī)中的一個(gè)線程對(duì)象 |
| stringID | 同 objectID 的長(zhǎng)度 | 表示 Target Java 虛擬機(jī)中的一字符串對(duì)象 |
| referenceTypeID | 同 objectID 的長(zhǎng)度 | 表示 Target Java 虛擬機(jī)中的一個(gè)引用類型對(duì)象,即類(class)的唯一 ID。 |
| classID | 同 objectID 的長(zhǎng)度 | 表示 Target Java 虛擬機(jī)中的一個(gè)類對(duì)象。 |
| methodID | 依據(jù) target Java 虛擬機(jī)而定,最大 8 byte | Target Java 虛擬機(jī)某個(gè)類中的方法的唯一 ID。methodID 必須在他所屬類和所屬類的所有子類中保持唯一。從整個(gè) Java 虛擬機(jī)來(lái)看它并不是唯一的。methodID 與它所屬類的 referenceTypeID 一起在整個(gè) Java 虛擬機(jī)中是唯一的。 |
| fieldID | 依據(jù) target Java 虛擬機(jī)而定,最大 8 byte | 與 methodID 類似,Target Java 虛擬機(jī)某個(gè)類中的成員的唯一 ID。 |
| frameID | 依據(jù) target Java 虛擬機(jī)而定,最大 8 byte | Java 中棧中的每一層方法調(diào)用都會(huì)生成一個(gè) frame。frameID 在整個(gè) target Java 虛擬機(jī)中是唯一的,并且只在線程掛起(suspended)的時(shí)候有效。 |
| location | 依據(jù) target Java 虛擬機(jī)而定,最大 8 byte | 一個(gè)可執(zhí)行的位置。Debugger 用它來(lái)定位 stepping 時(shí)在源代碼中的位置。 |
JDWP 傳輸接口(Java Debug Wire Protocol Transport Interface)
前面提到 JDWP 的定義是與傳輸層獨(dú)立的,但如何使 JDWP 能夠無(wú)縫的使用不同的傳輸實(shí)現(xiàn),而又無(wú)需修改 JDWP 本身的代碼? JDWP 傳輸接口(Java Debug Wire Protocol Transport Interface)為我們解決了這個(gè)問(wèn)題。
JDWP 傳輸接口定義了一系列的方法用來(lái)定義 JDWP 與傳輸層實(shí)現(xiàn)之間的交互方式。首先傳輸層的必須以動(dòng)態(tài)鏈接庫(kù)的方式實(shí)現(xiàn),并且暴露一系列的標(biāo)準(zhǔn)接口供 JDWP 使用。與 JNI 和 JVMTI 類似,訪問(wèn)傳輸層也需要一個(gè)環(huán)境指針(jdwpTransport),通過(guò)這個(gè)指針可以訪問(wèn)傳輸層提供的所有方法。
當(dāng) JDWP agent 被 Java 虛擬機(jī)加載后,JDWP 會(huì)根據(jù)參數(shù)去加載指定的傳輸層實(shí)現(xiàn)(Sun 的 JDK 在 Windows 提供 socket 和 share memory 兩種傳輸方式,而在 Linux 上只有 socket 方式)。傳輸層實(shí)現(xiàn)的動(dòng)態(tài)鏈接庫(kù)實(shí)現(xiàn)必須暴露 jdwpTransport_OnLoad 接口,JDWP agent 在加載傳輸層動(dòng)態(tài)鏈接庫(kù)后會(huì)調(diào)用該接口進(jìn)行傳輸層的初始化。接口定義如下:
JNIEXPORT jint JNICALL
jdwpTransport_OnLoad(JavaVM *jvm,
jdwpTransportCallback *callback,
jint version,
jdwpTransportEnv** env);
- callback 參數(shù)指向一個(gè)內(nèi)存管理的函數(shù)表,傳輸層用它來(lái)進(jìn)行內(nèi)存的分配和釋放,結(jié)構(gòu)定義如下:
typedef struct jdwpTransportCallback {
void* (*alloc)(jint numBytes);
void (*free)(void *buffer);
} jdwpTransportCallback;
- env 參數(shù)是環(huán)境指針,指向的函數(shù)表由傳輸層初始化。
JDWP 傳輸層定義的接口主要分為兩類:連接管理和 I/O 操作
連接管理
連接管理接口主要負(fù)責(zé)連接的建立和關(guān)閉。一個(gè)連接為 JDWP 和 debugger 提供了可靠的數(shù)據(jù)流。Packet 被接收的順序嚴(yán)格的按照被寫(xiě)入連接的順序。
連接的建立是雙向的,即 JDWP 可以主動(dòng)去連接 debugger 或者 JDWP 等待 debugger 的連接。對(duì)于主動(dòng)去連接 debugger,需要調(diào)用方法 Attach,定義如下:
jdwpTransportError
Attach(jdwpTransportEnv* env, const char* address,
jlong attachTimeout, jlong handshakeTimeout)
- 該方法將使 JDWP 處于監(jiān)聽(tīng)狀態(tài),隨后調(diào)用 Accept 方法接收連接:
jdwpTransportError
Accept(jdwpTransportEnv* env, jlong acceptTimeout, jlong
handshakeTimeout)
- 與 Attach 方法類似,在連接建立后,會(huì)立即進(jìn)行握手操作。
I/O 操作
- I/O 操作接口主要是負(fù)責(zé)從傳輸層讀寫(xiě) packet。有 ReadPacket 和 WritePacket 兩個(gè)方法:
jdwpTransportError
ReadPacket(jdwpTransportEnv* env, jdwpPacket* packet)
jdwpTransportError
WritePacket(jdwpTransportEnv* env, const jdwpPacket* packet)
- 參數(shù) packet 是要被讀寫(xiě)的 packet,其結(jié)構(gòu) jdwpPacket 與我們開(kāi)始提到的 JDWP packet 結(jié)構(gòu)一致,定義如下:
typedef struct {
jint len; // packet length
jint id; // packet id
jbyte flags; // value is 0
jbyte cmdSet; // command set
jbyte cmd; // command in specific command set
jbyte *data; // data carried by packet
} jdwpCmdPacket;
typedef struct {
jint len; // packet length
jint id; // packet id
jbyte flags; // value 0x80
jshort errorCode; // error code
jbyte *data; // data carried by packet
} jdwpReplyPacket;
typedef struct jdwpPacket {
union {
jdwpCmdPacket cmd;
jdwpReplyPacket reply;
} type;
} jdwpPacket;
JDWP 的命令實(shí)現(xiàn)機(jī)制
下面將通過(guò)講解一個(gè) JDWP 命令的實(shí)例來(lái)介紹 JDWP 命令的實(shí)現(xiàn)機(jī)制。JDWP 作為一種協(xié)議,它的作用就在于充當(dāng)了調(diào)試器與 Java 虛擬機(jī)的溝通橋梁。通俗點(diǎn)講,調(diào)試器在調(diào)試過(guò)程中需要不斷向 Java 虛擬機(jī)查詢各種信息,那么 JDWP 就規(guī)定了查詢的具體方式。
在 Java 6.0 中,JDWP 包含了 18 組命令集合,其中每個(gè)命令集合又包含了若干條命令。那么這些命令是如何實(shí)現(xiàn)的呢?下面我們先來(lái)看一個(gè)最簡(jiǎn)單的 VirtualMachine(命令集合 1)的 Version 命令,以此來(lái)剖析其中的實(shí)現(xiàn)細(xì)節(jié)。
因?yàn)?JDWP 在整個(gè) JPDA 框架中處于相對(duì)底層的位置(在前兩篇本系列文章中有具體說(shuō)明),我們無(wú)法在現(xiàn)實(shí)應(yīng)用中來(lái)為大家演示 JDWP 的單個(gè)命令的執(zhí)行過(guò)程。在這里我們通過(guò)一個(gè)針對(duì)該命令的 Java 測(cè)試用例來(lái)說(shuō)明。
CommandPacket packet = new CommandPacket(
JDWPCommands.VirtualMachineCommandSet.CommandSetID,
JDWPCommands.VirtualMachineCommandSet.VersionCommand);
ReplyPacket reply = debuggeeWrapper.vmMirror.performCommand(packet);
String description = reply.getNextValueAsString();
int jdwpMajor = reply.getNextValueAsInt();
int jdwpMinor = reply.getNextValueAsInt();
String vmVersion = reply.getNextValueAsString();
String vmName = reply.getNextValueAsString();
logWriter.println("description\t= " + description);
logWriter.println("jdwpMajor\t= " + jdwpMajor);
logWriter.println("jdwpMinor\t= " + jdwpMinor);
logWriter.println("vmVersion\t= " + vmVersion);
logWriter.println("vmName\t\t= " + vmName);
這里先簡(jiǎn)單介紹一下這段代碼的作用。
首先,我們會(huì)創(chuàng)建一個(gè) VirtualMachine 的 Version 命令的命令包實(shí)例 packet。你可能已經(jīng)注意到,該命令包主要就是配置了兩個(gè)參數(shù) : CommandSetID 和 VersionComamnd,它們的值均為 1。表明我們想執(zhí)行的命令是屬于命令集合 1 的命令 1,即 VirtualMachine 的 Version 命令。然后在 performCommand 方法中我們發(fā)送了該命令并收到了 JDWP 的回復(fù)包 reply。通過(guò)解析 reply,我們得到了該命令的回復(fù)信息。
description = Java 虛擬機(jī) version 1.6.0 (IBM J9 VM, J2RE 1.6.0 IBM J9 2.4 Windows XP x86-32
jvmwi3260sr5-20090519_35743 (JIT enabled, AOT enabled)
J9VM - 20090519_035743_lHdSMr
JIT - r9_20090518_2017
GC - 20090417_AA, 2.4)
jdwpMajor = 1
jdwpMinor = 6
vmVersion = 1.6.0
vmName = IBM J9 VM
測(cè)試用例的執(zhí)行結(jié)果顯示,我們通過(guò)該命令獲得了 Java 虛擬機(jī)的版本信息,這正是 VirtualMachine 的 Version 命令的作用。
前面已經(jīng)提到,JDWP 接收到的是調(diào)試器發(fā)送的命令包,返回的就是反饋信息的回復(fù)包。在這個(gè)例子中,我們模擬的調(diào)試器會(huì)發(fā)送 VirtualMachine 的 Version 命令。JDWP 在執(zhí)行完該命令后就向調(diào)試器返回 Java 虛擬機(jī)的版本信息。
返回信息的包內(nèi)容同樣是在 JDWP Spec 里面規(guī)定的。比如本例中的回復(fù)包,Spec 中的描述如下(測(cè)試用例中的回復(fù)包解析就是參照這個(gè)規(guī)定的 ):
- 表 2. VirtualMachine 的 Version 命令返回包定義
| 類型 | 名稱 | 說(shuō)明 |
|---|---|---|
| string | description | VM version 的文字描述信息。 |
| int | jdwpMajor | JDWP 主版本號(hào)。 |
| int | jdwpMinor | JDWP 次版本號(hào)。 |
| string | vmVersion | VM JRE 版本,也就是 java.version 屬性值。 |
| string | vmName | VM 的名稱,也就是 java.vm.name 屬性值。 |
通過(guò)這個(gè)簡(jiǎn)單的例子,相信大家對(duì) JDWP 的命令已經(jīng)有了一個(gè)大體的了解。 那么在 JDWP 內(nèi)部是如何處理接收到的命令并返回回復(fù)包的呢?下面以 Apache Harmony 的 JDWP 為例,為大家介紹其內(nèi)部的實(shí)現(xiàn)架構(gòu)。
-
圖 5. JDWP 架構(gòu)圖
JDWP 架構(gòu)圖 -
圖 6. JDWP 命令處理流程
JDWP 命令處理流程
如圖所示,JDWP 接收和發(fā)送的包都會(huì)經(jīng)過(guò) TransportManager 進(jìn)行處理。JDWP 的應(yīng)用層與傳輸層是獨(dú)立的,就在于 TransportManager 調(diào)用的是 JDWP 傳輸接口(Java Debug Wire Protocol Transport Interface),所以無(wú)需關(guān)心底層網(wǎng)絡(luò)的具體傳輸實(shí)現(xiàn)。TransportManager 的主要作用就是充當(dāng) JDWP 與外界通訊的數(shù)據(jù)包的中轉(zhuǎn)站,負(fù)責(zé)將 JDWP 的命令包在接收后進(jìn)行解析或是對(duì)回復(fù)包在發(fā)送前進(jìn)行打包,從而使 JDWP 能夠?qū)W⒂趹?yīng)用層的實(shí)現(xiàn)。
對(duì)于收到的命令包,TransportManager 處理后會(huì)轉(zhuǎn)給 PacketDispatcher,進(jìn)一步封裝后會(huì)繼續(xù)轉(zhuǎn)到 CommandDispatcher。然后,CommandDispatcher 會(huì)根據(jù)命令中提供的命令組號(hào)和命令號(hào)創(chuàng)建一個(gè)具體的 CommandHandler 來(lái)處理 JDWP 命令。
其中,CommandHandler 才是真正執(zhí)行 JDWP 命令的類。我們會(huì)為每個(gè) JDWP 命令都定義一個(gè)相對(duì)應(yīng)的 CommandHandler 的子類,當(dāng)接收到某個(gè)命令時(shí),就會(huì)創(chuàng)建處理該命令的 CommandHandler 的子類的實(shí)例來(lái)作具體的處理。
單線程執(zhí)行的命令
上圖就是一個(gè)命令的處理流程圖??梢钥吹?,對(duì)于一個(gè)可以直接在該線程中完成的命令(我們稱為單線程執(zhí)行的命令),一般其內(nèi)部會(huì)調(diào)用 JVMTI 方法和 JNI 方法來(lái)真正對(duì) Java 虛擬機(jī)進(jìn)行操作。
例如,VirtualMachine 的 Version 命令中,對(duì)于 vmVersion 和 vmName 屬性,我們可以通過(guò) JNI 來(lái)調(diào)用 Java 方法 System.getProperty 來(lái)獲取。然后,JDWP 將回復(fù)包中所需要的結(jié)果封裝到包中后交由 TransportManager 來(lái)進(jìn)行后續(xù)操作。
多線程執(zhí)行的命令
對(duì)于一些較為復(fù)雜的命令,是無(wú)法在 CommandHandler 子類的處理線程中完成的。例如,ClassType 的 InvokeMethod 命令,它會(huì)要求在指定的某個(gè)線程中執(zhí)行一個(gè)靜態(tài)方法。顯然,CommandHandler 子類的當(dāng)前線程并不是所要求的線程。這時(shí),JDWP 線程會(huì)先把這個(gè)請(qǐng)求先放到一個(gè)列表中,然后等待,直到所要求的線程執(zhí)行完那個(gè)靜態(tài)方法后,再把結(jié)果返回給調(diào)試器。
JDWP 的事件處理機(jī)制
前面介紹的 VirtualMachine 的 Version 命令過(guò)程非常簡(jiǎn)單,就是一個(gè)查詢和信息返回的過(guò)程。在實(shí)際調(diào)試過(guò)程中,一個(gè) JDI 的命令往往會(huì)有數(shù)條這類簡(jiǎn)單的查詢命令參與,而且會(huì)涉及到很多更為復(fù)雜的命令。要了解更為復(fù)雜的 JDWP 命令實(shí)現(xiàn)機(jī)制,就必須介紹 JDWP 的事件處理機(jī)制。
在 Java 虛擬機(jī)中,我們會(huì)接觸到許多事件,例如 VM 的初始化,類的裝載,異常的發(fā)生,斷點(diǎn)的觸發(fā)等等。那么這些事件調(diào)試器是如何通過(guò) JDWP 來(lái)獲知的呢?下面,我們通過(guò)介紹在調(diào)試過(guò)程中斷點(diǎn)的觸發(fā)是如何實(shí)現(xiàn)的,來(lái)為大家揭示其中的實(shí)現(xiàn)機(jī)制。
在這里,我們?nèi)我庹{(diào)試一段 Java 程序,并在某一行中加入斷點(diǎn)。然后,我們執(zhí)行到該斷點(diǎn),此時(shí)所有 Java 線程都處于 suspend 狀態(tài)。這是很常見(jiàn)的斷點(diǎn)觸發(fā)過(guò)程。為了記錄在此過(guò)程中 JDWP 的行為,我們使用了一個(gè)開(kāi)啟了 trace 信息的 JDWP。雖然這并不是一個(gè)復(fù)雜的操作,但整個(gè) trace 信息也有幾千行??梢?jiàn),作為相對(duì)底層的 JDWP,其實(shí)際處理的命令要比想象的多許多。為了介紹 JDWP 的事件處理機(jī)制,我們挑選了其中比較重要的一些 trace 信息來(lái)說(shuō)明:
[RequestManager.cpp:601] AddRequest: event=BREAKPOINT[2], req=48, modCount=1, policy=1
[RequestManager.cpp:791] GenerateEvents: event #0: kind=BREAKPOINT, req=48
[RequestManager.cpp:1543] HandleBreakpoint: BREAKPOINT events: count=1, suspendPolicy=1,
location=0
[RequestManager.cpp:1575] HandleBreakpoint: post set of 1
[EventDispatcher.cpp:415] PostEventSet -- wait for release on event: thread=4185A5A0,
name=(null), eventKind=2
[EventDispatcher.cpp:309] SuspendOnEvent -- send event set: id=3, policy=1
[EventDispatcher.cpp:334] SuspendOnEvent -- wait for thread on event: thread=4185A5A0,
name=(null)
[EventDispatcher.cpp:349] SuspendOnEvent -- suspend thread on event: thread=4185A5A0,
name=(null)
[EventDispatcher.cpp:360] SuspendOnEvent -- release thread on event: thread=4185A5A0,
name=(null)
首先,調(diào)試器需要發(fā)起一個(gè)斷點(diǎn)的請(qǐng)求,這是通過(guò) JDWP 的 Set 命令完成的。在 trace 中,我們看到 AddRequest 就是做了這件事??梢郧宄陌l(fā)現(xiàn),調(diào)試器請(qǐng)求的是一個(gè)斷點(diǎn)信息(event=BREAKPOINT[2])。
在 JDWP 的實(shí)現(xiàn)中,這一過(guò)程表現(xiàn)為:在 Set 命令中會(huì)生成一個(gè)具體的 request, JDWP 的 RequestManager 會(huì)記錄這個(gè) request(request 中會(huì)包含一些過(guò)濾條件,當(dāng)事件發(fā)生時(shí) RequestManager 會(huì)過(guò)濾掉不符合預(yù)先設(shè)定條件的事件),并通過(guò) JVMTI 的 SetEventNotificationMode 方法使這個(gè)事件觸發(fā)生效(否則事件發(fā)生時(shí) Java 虛擬機(jī)不會(huì)報(bào)告)。
-
圖 7. JDWP 事件處理流程
JDWP 事件處理流程
當(dāng)斷點(diǎn)發(fā)生時(shí),Java 虛擬機(jī)就會(huì)調(diào)用 JDWP 中預(yù)先定義好的處理該事件的回調(diào)函數(shù)。在 trace 中,HandleBreakpoint 就是我們?cè)?JDWP 中定義好的處理斷點(diǎn)信息的回調(diào)函數(shù)。它的作用就是要生成一個(gè) JDWP 端所描述的斷點(diǎn)事件來(lái)告知調(diào)試器(Java 虛擬機(jī)只是觸發(fā)了一個(gè) JVMTI 的消息)。
由于斷點(diǎn)的事件在調(diào)試器申請(qǐng)時(shí)就要求所有 Java 線程在斷點(diǎn)觸發(fā)時(shí)被 suspend,那這一步由誰(shuí)來(lái)完成呢?這里要談到一個(gè)細(xì)節(jié)問(wèn)題,HandleBreakpoint 作為一個(gè)回調(diào)函數(shù),其執(zhí)行線程其實(shí)就是斷點(diǎn)觸發(fā)的 Java 線程。
顯然,我們不應(yīng)該由它來(lái)負(fù)責(zé) suspend 所有 Java 線程。
原因很簡(jiǎn)單,我們還有一步工作要做,就是要把該斷點(diǎn)觸發(fā)信息返回給調(diào)試器。如果我們先返回信息,然后 suspend 所有 Java 線程,這就無(wú)法保證在調(diào)試器收到信息時(shí)所有 Java 線程已經(jīng)被 suspend。
反之,先 Suspend 了所有 Java 線程,誰(shuí)來(lái)負(fù)責(zé)發(fā)送信息給調(diào)試器呢?
為了解決這個(gè)問(wèn)題,我們通過(guò) JDWP 的 EventDispatcher 線程來(lái)幫我們 suspend 線程和發(fā)送信息。實(shí)現(xiàn)的過(guò)程是,我們讓觸發(fā)斷點(diǎn)的 Java 線程來(lái) PostEventSet(trace 中可以看到),把生成的 JDWP 事件放到一個(gè)隊(duì)列中,然后就開(kāi)始等待。由 EventDispatcher 線程來(lái)負(fù)責(zé)從隊(duì)列中取出 JDWP 事件,并根據(jù)事件中的設(shè)定,來(lái) suspend 所要求的 Java 線程并發(fā)送出該事件。
在這里,我們?cè)谑录|發(fā)的 Java 線程和 EventDispatcher 線程之間添加了一個(gè)同步機(jī)制,當(dāng)事件發(fā)送出去后,事件觸發(fā)的 Java 線程會(huì)把 JDWP 中的該事件刪除,到這里,整個(gè) JDWP 事件處理就完成了
結(jié)語(yǔ)
我們?cè)谡{(diào)試 Java 程序的時(shí)候,往往需要對(duì)虛擬機(jī)內(nèi)部的運(yùn)行狀態(tài)進(jìn)行觀察和調(diào)試,JDWP Agent 就充當(dāng)了調(diào)試器與 Java 虛擬機(jī)的溝通橋梁。它的工作原理簡(jiǎn)單來(lái)說(shuō)就是對(duì)于 JDWP 命令的處理和事件的管理。由于 JDWP 在 JPDA 中處于相對(duì)底層的位置,調(diào)試器發(fā)出一個(gè) JDI 指令,往往要通過(guò)很多 JDWP 命令來(lái)完成,
此外由于兩年前在做一個(gè)項(xiàng)目,然后發(fā)現(xiàn)生產(chǎn)環(huán)境與本地開(kāi)發(fā)環(huán)境一致的代碼,運(yùn)行結(jié)果完全不一樣,然后就通過(guò)Eclipse進(jìn)行Remote遠(yuǎn)程調(diào)代碼調(diào)試進(jìn)行跟蹤,由此留下比較深刻的印象,看到此文章比較故進(jìn)行整理,隨便把IDEA和Eclipse的遠(yuǎn)程調(diào)試截圖標(biāo)記一下。
來(lái)自文章






