什么是JDWP 甸鸟?
JDWP 是 Java Debug Wire Protocol 的縮寫前翎,它定義了調(diào)試器(debugger)和被調(diào)試的 Java 虛擬機(target vm)之間的通信協(xié)議。
JDWP 協(xié)議介紹
這里首先要說明一下 debugger 和 target vm。Target vm 中運行著我們希望要調(diào)試的程序,它與一般運行的 Java 虛擬機沒有什么區(qū)別,只是在啟動時加載了 Agent JDWP 從而具備了調(diào)試功能磨淌。而 debugger 就是我們熟知的調(diào)試器硫豆,它向運行中的 target vm 發(fā)送命令來獲取 target vm 運行時的狀態(tài)和控制 Java 程序的執(zhí)行。Debugger 和 target vm 分別在各自的進程中運行无拗,他們之間的通信協(xié)議就是 JDWP带到。
JDWP 與其他許多協(xié)議不同,它僅僅定義了數(shù)據(jù)傳輸?shù)母袷接⑷荆]有指定具體的傳輸方式揽惹。這就意味著一個 JDWP 的實現(xiàn)可以不需要做任何修改就正常工作在不同的傳輸方式上(在 JDWP 傳輸接口中會做詳細(xì)介紹)。
JDWP 是語言無關(guān)的四康。理論上我們可以選用任意語言實現(xiàn) JDWP搪搏。然而我們注意到,在 JDWP 的兩端分別是 target vm 和 debugger闪金。Target vm 端疯溺,JDWP 模塊必須以 Agent library 的形式在 Java 虛擬機啟動時加載,并且它必須通過 Java 虛擬機提供的 JVMTI 接口實現(xiàn)各種 debug 的功能哎垦,所以必須使用 C/C++ 語言編寫囱嫩。而 debugger 端就沒有這樣的限制,可以使用任意語言編寫漏设,只要遵守 JDWP 規(guī)范即可墨闲。JDI(Java Debug Interface)就包含了一個 Java 的 JDWP debugger 端的實現(xiàn)(JDI 將在該系列的下一篇文章中介紹),JDK 中調(diào)試工具 jdb 也是使用 JDI 完成其調(diào)試功能的郑口。
圖 1. JDWP agent 在調(diào)試中扮演的角色
協(xié)議分析
JDWP 大致分為兩個階段:握手和應(yīng)答鸳碧。握手是在傳輸層連接建立完成后,做的第一件事:
Debugger 發(fā)送 14 bytes 的字符串“JDWP-Handshake”到 target Java 虛擬機
Target Java 虛擬機回復(fù)“JDWP-Handshake”
圖 2. JDWP 的握手協(xié)議
握手完成潘酗,debugger 就可以向 target Java 虛擬機發(fā)送命令了杆兵。JDWP 是通過命令(command)和回復(fù)(reply)進行通信的,這與 HTTP 有些相似仔夺。JDWP 本身是無狀態(tài)的琐脏,因此對 command 出現(xiàn)的順序并不受限制。JDWP 有兩種基本的包(packet)類型:命令包(command packet)和回復(fù)包(reply packet)缸兔。
Debugger 和 target Java 虛擬機都有可能發(fā)送 command packet日裙。Debugger 通過發(fā)送 command packet 獲取 target Java 虛擬機的信息以及控制程序的執(zhí)行。Target Java 虛擬機通過發(fā)送 command packet 通知 debugger 某些事件的發(fā)生惰蜜,如到達斷點或是產(chǎn)生異常昂拂。
Reply packet 是用來回復(fù) command packet 該命令是否執(zhí)行成功,如果成功 reply packet 還有可能包含 command packet 請求的數(shù)據(jù)抛猖,比如當(dāng)前的線程信息或者變量的值格侯。從 target Java 虛擬機發(fā)送的事件消息是不需要回復(fù)的鼻听。還有一點需要注意的是,JDWP 是異步的:command packet 的發(fā)送方不需要等待接收到 reply packet 就可以繼續(xù)發(fā)送下一個 command packet联四。
Packet 的結(jié)構(gòu)
Packet 分為包頭(header)和數(shù)據(jù)(data)兩部分組成撑碴。包頭部分的結(jié)構(gòu)和長度是固定,而數(shù)據(jù)部分的長度是可變的朝墩,具體內(nèi)容視 packet 的內(nèi)容而定醉拓。Command packet 和 reply packet 的包頭長度相同,都是 11 個 bytes收苏,這樣更有利于傳輸層的抽象和實現(xiàn)亿卤。
- Command packet 的 header 的結(jié)構(gòu) :
-
圖 3. JDWP command packet 結(jié)構(gòu)
JDWP command packet 結(jié)構(gòu)
Length 是整個 packet 的長度,包括 length 部分鹿霸。因為包頭的長度是固定的 11 bytes排吴,所以如果一個 command packet 沒有數(shù)據(jù)部分,則 length 的值就是 11杜跷。
Id 是一個唯一值傍念,用來標(biāo)記和識別 reply 所屬的 command矫夷。Reply packet 與它所回復(fù)的 command packet 具有相同的 Id葛闷,異步的消息就是通過 Id 來配對識別的。
Flags 目前對于 command packet 值始終是 0双藕。
Command Set 相當(dāng)于一個 command 的分組淑趾,一些功能相近的 command 被分在同一個 Command Set 中。Command Set 的值被劃分為 3 個部分:
0-63: 從 debugger 發(fā)往 target Java 虛擬機的命令
64 – 127: 從 target Java 虛擬機發(fā)往 debugger 的命令
128 – 256: 預(yù)留的自定義和擴展命令
- Reply packet 的 header 的結(jié)構(gòu):
-
圖 4. JDWP reply packet 結(jié)構(gòu)
JDWP reply packet 結(jié)構(gòu)
Length忧陪、Id 作用與 command packet 中的一樣扣泊。
Flags 目前對于 reply packet 值始終是 0x80。我們可以通過 Flags 的值來判斷接收到的 packet 是 command 還是 reply嘶摊。
Error Code 用來表示被回復(fù)的命令是否被正確執(zhí)行了延蟹。零表示正確,非零表示執(zhí)行錯誤叶堆。
Data 的內(nèi)容和結(jié)構(gòu)依據(jù)不同的 command 和 reply 都有所不同阱飘。比如請求一個對象成員變量值的 command,它的 data 中就包含該對象的 id 和成員變量的 id虱颗。而 reply 中則包含該成員變量的值沥匈。
JDWP 還定義了一些數(shù)據(jù)類型專門用來傳遞 Java 相關(guān)的數(shù)據(jù)信息。下面列舉了一些數(shù)據(jù)類型忘渔,詳細(xì)的說明參見 [1]
- 表 1. JDWP 中數(shù)據(jù)類型介紹
名稱 | 長度 | 說明 |
---|---|---|
byte | 1 byte | byte 值高帖。 |
boolean | 1 byte | 布爾值,0 表示假畦粮,非零表示真散址。 |
int | 4 byte | 4 字節(jié)有符號整數(shù)乖阵。 |
long | 8 byte | 8 字節(jié)有符號整數(shù)。 |
objectID | 依據(jù) target Java 虛擬機而定预麸,最大 8 byte | Target Java 虛擬機中對象(object)的唯一 ID义起。這個值在整個 JDWP 的會話中不會被重用,始終指向同一個對象师崎,即使該對象已經(jīng)被 GC 回收(引用被回收的對象將返回 INVALID_OBJECT 錯誤默终。 |
Tagged-objectID | objectID 的長度加 1 | 第一個 byte 表示對象的類型,比如犁罩,整型齐蔽,字符串,類等等床估。緊接著是一個 objectID含滴。 |
threadID | 同 objectID 的長度 | 表示 Target Java 虛擬機中的一個線程對象 |
stringID | 同 objectID 的長度 | 表示 Target Java 虛擬機中的一字符串對象 |
referenceTypeID | 同 objectID 的長度 | 表示 Target Java 虛擬機中的一個引用類型對象,即類(class)的唯一 ID丐巫。 |
classID | 同 objectID 的長度 | 表示 Target Java 虛擬機中的一個類對象谈况。 |
methodID | 依據(jù) target Java 虛擬機而定,最大 8 byte | Target Java 虛擬機某個類中的方法的唯一 ID递胧。methodID 必須在他所屬類和所屬類的所有子類中保持唯一碑韵。從整個 Java 虛擬機來看它并不是唯一的。methodID 與它所屬類的 referenceTypeID 一起在整個 Java 虛擬機中是唯一的缎脾。 |
fieldID | 依據(jù) target Java 虛擬機而定祝闻,最大 8 byte | 與 methodID 類似,Target Java 虛擬機某個類中的成員的唯一 ID遗菠。 |
frameID | 依據(jù) target Java 虛擬機而定联喘,最大 8 byte | Java 中棧中的每一層方法調(diào)用都會生成一個 frame。frameID 在整個 target Java 虛擬機中是唯一的辙纬,并且只在線程掛起(suspended)的時候有效豁遭。 |
location | 依據(jù) target Java 虛擬機而定,最大 8 byte | 一個可執(zhí)行的位置贺拣。Debugger 用它來定位 stepping 時在源代碼中的位置蓖谢。 |
JDWP 傳輸接口(Java Debug Wire Protocol Transport Interface)
前面提到 JDWP 的定義是與傳輸層獨立的,但如何使 JDWP 能夠無縫的使用不同的傳輸實現(xiàn)纵柿,而又無需修改 JDWP 本身的代碼蜈抓? JDWP 傳輸接口(Java Debug Wire Protocol Transport Interface)為我們解決了這個問題。
JDWP 傳輸接口定義了一系列的方法用來定義 JDWP 與傳輸層實現(xiàn)之間的交互方式昂儒。首先傳輸層的必須以動態(tài)鏈接庫的方式實現(xiàn)沟使,并且暴露一系列的標(biāo)準(zhǔn)接口供 JDWP 使用。與 JNI 和 JVMTI 類似渊跋,訪問傳輸層也需要一個環(huán)境指針(jdwpTransport)腊嗡,通過這個指針可以訪問傳輸層提供的所有方法着倾。
當(dāng) JDWP agent 被 Java 虛擬機加載后,JDWP 會根據(jù)參數(shù)去加載指定的傳輸層實現(xiàn)(Sun 的 JDK 在 Windows 提供 socket 和 share memory 兩種傳輸方式燕少,而在 Linux 上只有 socket 方式)卡者。傳輸層實現(xiàn)的動態(tài)鏈接庫實現(xiàn)必須暴露 jdwpTransport_OnLoad 接口,JDWP agent 在加載傳輸層動態(tài)鏈接庫后會調(diào)用該接口進行傳輸層的初始化客们。接口定義如下:
JNIEXPORT jint JNICALL
jdwpTransport_OnLoad(JavaVM *jvm,
jdwpTransportCallback *callback,
jint version,
jdwpTransportEnv** env);
- callback 參數(shù)指向一個內(nèi)存管理的函數(shù)表崇决,傳輸層用它來進行內(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)閉。一個連接為 JDWP 和 debugger 提供了可靠的數(shù)據(jù)流建邓。Packet 被接收的順序嚴(yán)格的按照被寫入連接的順序盈厘。
連接的建立是雙向的,即 JDWP 可以主動去連接 debugger 或者 JDWP 等待 debugger 的連接官边。對于主動去連接 debugger沸手,需要調(diào)用方法 Attach,定義如下:
jdwpTransportError
Attach(jdwpTransportEnv* env, const char* address,
jlong attachTimeout, jlong handshakeTimeout)
- 該方法將使 JDWP 處于監(jiān)聽狀態(tài)注簿,隨后調(diào)用 Accept 方法接收連接:
jdwpTransportError
Accept(jdwpTransportEnv* env, jlong acceptTimeout, jlong
handshakeTimeout)
- 與 Attach 方法類似契吉,在連接建立后,會立即進行握手操作滩援。
I/O 操作
- I/O 操作接口主要是負(fù)責(zé)從傳輸層讀寫 packet栅隐。有 ReadPacket 和 WritePacket 兩個方法:
jdwpTransportError
ReadPacket(jdwpTransportEnv* env, jdwpPacket* packet)
jdwpTransportError
WritePacket(jdwpTransportEnv* env, const jdwpPacket* packet)
- 參數(shù) packet 是要被讀寫的 packet,其結(jié)構(gòu) jdwpPacket 與我們開始提到的 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 的命令實現(xiàn)機制
下面將通過講解一個 JDWP 命令的實例來介紹 JDWP 命令的實現(xiàn)機制。JDWP 作為一種協(xié)議谨究,它的作用就在于充當(dāng)了調(diào)試器與 Java 虛擬機的溝通橋梁恩袱。通俗點講,調(diào)試器在調(diào)試過程中需要不斷向 Java 虛擬機查詢各種信息胶哲,那么 JDWP 就規(guī)定了查詢的具體方式畔塔。
在 Java 6.0 中,JDWP 包含了 18 組命令集合鸯屿,其中每個命令集合又包含了若干條命令澈吨。那么這些命令是如何實現(xiàn)的呢?下面我們先來看一個最簡單的 VirtualMachine(命令集合 1)的 Version 命令寄摆,以此來剖析其中的實現(xiàn)細(xì)節(jié)谅辣。
因為 JDWP 在整個 JPDA 框架中處于相對底層的位置(在前兩篇本系列文章中有具體說明),我們無法在現(xiàn)實應(yīng)用中來為大家演示 JDWP 的單個命令的執(zhí)行過程婶恼。在這里我們通過一個針對該命令的 Java 測試用例來說明桑阶。
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);
這里先簡單介紹一下這段代碼的作用柏副。
首先,我們會創(chuàng)建一個 VirtualMachine 的 Version 命令的命令包實例 packet蚣录。你可能已經(jīng)注意到割择,該命令包主要就是配置了兩個參數(shù) : CommandSetID 和 VersionComamnd,它們的值均為 1萎河。表明我們想執(zhí)行的命令是屬于命令集合 1 的命令 1荔泳,即 VirtualMachine 的 Version 命令。然后在 performCommand 方法中我們發(fā)送了該命令并收到了 JDWP 的回復(fù)包 reply虐杯。通過解析 reply换可,我們得到了該命令的回復(fù)信息。
description = Java 虛擬機 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
測試用例的執(zhí)行結(jié)果顯示厦幅,我們通過該命令獲得了 Java 虛擬機的版本信息沾鳄,這正是 VirtualMachine 的 Version 命令的作用。
前面已經(jīng)提到确憨,JDWP 接收到的是調(diào)試器發(fā)送的命令包译荞,返回的就是反饋信息的回復(fù)包。在這個例子中休弃,我們模擬的調(diào)試器會發(fā)送 VirtualMachine 的 Version 命令吞歼。JDWP 在執(zhí)行完該命令后就向調(diào)試器返回 Java 虛擬機的版本信息。
返回信息的包內(nèi)容同樣是在 JDWP Spec 里面規(guī)定的塔猾。比如本例中的回復(fù)包篙骡,Spec 中的描述如下(測試用例中的回復(fù)包解析就是參照這個規(guī)定的 ):
- 表 2. VirtualMachine 的 Version 命令返回包定義
類型 | 名稱 | 說明 |
---|---|---|
string | description | VM version 的文字描述信息。 |
int | jdwpMajor | JDWP 主版本號丈甸。 |
int | jdwpMinor | JDWP 次版本號糯俗。 |
string | vmVersion | VM JRE 版本,也就是 java.version 屬性值睦擂。 |
string | vmName | VM 的名稱得湘,也就是 java.vm.name 屬性值。 |
通過這個簡單的例子顿仇,相信大家對 JDWP 的命令已經(jīng)有了一個大體的了解淘正。 那么在 JDWP 內(nèi)部是如何處理接收到的命令并返回回復(fù)包的呢?下面以 Apache Harmony 的 JDWP 為例臼闻,為大家介紹其內(nèi)部的實現(xiàn)架構(gòu)鸿吆。
-
圖 5. JDWP 架構(gòu)圖
JDWP 架構(gòu)圖 -
圖 6. JDWP 命令處理流程
JDWP 命令處理流程
如圖所示,JDWP 接收和發(fā)送的包都會經(jīng)過 TransportManager 進行處理述呐。JDWP 的應(yīng)用層與傳輸層是獨立的惩淳,就在于 TransportManager 調(diào)用的是 JDWP 傳輸接口(Java Debug Wire Protocol Transport Interface),所以無需關(guān)心底層網(wǎng)絡(luò)的具體傳輸實現(xiàn)市埋。TransportManager 的主要作用就是充當(dāng) JDWP 與外界通訊的數(shù)據(jù)包的中轉(zhuǎn)站黎泣,負(fù)責(zé)將 JDWP 的命令包在接收后進行解析或是對回復(fù)包在發(fā)送前進行打包恕刘,從而使 JDWP 能夠?qū)W⒂趹?yīng)用層的實現(xiàn)。
對于收到的命令包抒倚,TransportManager 處理后會轉(zhuǎn)給 PacketDispatcher褐着,進一步封裝后會繼續(xù)轉(zhuǎn)到 CommandDispatcher。然后托呕,CommandDispatcher 會根據(jù)命令中提供的命令組號和命令號創(chuàng)建一個具體的 CommandHandler 來處理 JDWP 命令含蓉。
其中,CommandHandler 才是真正執(zhí)行 JDWP 命令的類项郊。我們會為每個 JDWP 命令都定義一個相對應(yīng)的 CommandHandler 的子類馅扣,當(dāng)接收到某個命令時,就會創(chuàng)建處理該命令的 CommandHandler 的子類的實例來作具體的處理着降。
單線程執(zhí)行的命令
上圖就是一個命令的處理流程圖差油。可以看到任洞,對于一個可以直接在該線程中完成的命令(我們稱為單線程執(zhí)行的命令)蓄喇,一般其內(nèi)部會調(diào)用 JVMTI 方法和 JNI 方法來真正對 Java 虛擬機進行操作。
例如交掏,VirtualMachine 的 Version 命令中妆偏,對于 vmVersion 和 vmName 屬性,我們可以通過 JNI 來調(diào)用 Java 方法 System.getProperty 來獲取盅弛。然后钱骂,JDWP 將回復(fù)包中所需要的結(jié)果封裝到包中后交由 TransportManager 來進行后續(xù)操作。
多線程執(zhí)行的命令
對于一些較為復(fù)雜的命令挪鹏,是無法在 CommandHandler 子類的處理線程中完成的见秽。例如,ClassType 的 InvokeMethod 命令狰住,它會要求在指定的某個線程中執(zhí)行一個靜態(tài)方法张吉。顯然,CommandHandler 子類的當(dāng)前線程并不是所要求的線程催植。這時,JDWP 線程會先把這個請求先放到一個列表中勺择,然后等待创南,直到所要求的線程執(zhí)行完那個靜態(tài)方法后,再把結(jié)果返回給調(diào)試器省核。
JDWP 的事件處理機制
前面介紹的 VirtualMachine 的 Version 命令過程非常簡單稿辙,就是一個查詢和信息返回的過程。在實際調(diào)試過程中气忠,一個 JDI 的命令往往會有數(shù)條這類簡單的查詢命令參與邻储,而且會涉及到很多更為復(fù)雜的命令赋咽。要了解更為復(fù)雜的 JDWP 命令實現(xiàn)機制,就必須介紹 JDWP 的事件處理機制吨娜。
在 Java 虛擬機中脓匿,我們會接觸到許多事件,例如 VM 的初始化宦赠,類的裝載陪毡,異常的發(fā)生,斷點的觸發(fā)等等勾扭。那么這些事件調(diào)試器是如何通過 JDWP 來獲知的呢毡琉?下面,我們通過介紹在調(diào)試過程中斷點的觸發(fā)是如何實現(xiàn)的妙色,來為大家揭示其中的實現(xiàn)機制桅滋。
在這里,我們?nèi)我庹{(diào)試一段 Java 程序身辨,并在某一行中加入斷點丐谋。然后,我們執(zhí)行到該斷點栅表,此時所有 Java 線程都處于 suspend 狀態(tài)笋鄙。這是很常見的斷點觸發(fā)過程。為了記錄在此過程中 JDWP 的行為怪瓶,我們使用了一個開啟了 trace 信息的 JDWP萧落。雖然這并不是一個復(fù)雜的操作,但整個 trace 信息也有幾千行洗贰≌裔可見,作為相對底層的 JDWP敛滋,其實際處理的命令要比想象的多許多许布。為了介紹 JDWP 的事件處理機制,我們挑選了其中比較重要的一些 trace 信息來說明:
[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ā)起一個斷點的請求蜜唾,這是通過 JDWP 的 Set 命令完成的。在 trace 中庶艾,我們看到 AddRequest 就是做了這件事袁余。可以清楚的發(fā)現(xiàn)咱揍,調(diào)試器請求的是一個斷點信息(event=BREAKPOINT[2])颖榜。
在 JDWP 的實現(xiàn)中,這一過程表現(xiàn)為:在 Set 命令中會生成一個具體的 request, JDWP 的 RequestManager 會記錄這個 request(request 中會包含一些過濾條件,當(dāng)事件發(fā)生時 RequestManager 會過濾掉不符合預(yù)先設(shè)定條件的事件)掩完,并通過 JVMTI 的 SetEventNotificationMode 方法使這個事件觸發(fā)生效(否則事件發(fā)生時 Java 虛擬機不會報告)噪漾。
-
圖 7. JDWP 事件處理流程
JDWP 事件處理流程
當(dāng)斷點發(fā)生時,Java 虛擬機就會調(diào)用 JDWP 中預(yù)先定義好的處理該事件的回調(diào)函數(shù)且蓬。在 trace 中欣硼,HandleBreakpoint 就是我們在 JDWP 中定義好的處理斷點信息的回調(diào)函數(shù)。它的作用就是要生成一個 JDWP 端所描述的斷點事件來告知調(diào)試器(Java 虛擬機只是觸發(fā)了一個 JVMTI 的消息)缅疟。
由于斷點的事件在調(diào)試器申請時就要求所有 Java 線程在斷點觸發(fā)時被 suspend分别,那這一步由誰來完成呢?這里要談到一個細(xì)節(jié)問題存淫,HandleBreakpoint 作為一個回調(diào)函數(shù)耘斩,其執(zhí)行線程其實就是斷點觸發(fā)的 Java 線程。
顯然桅咆,我們不應(yīng)該由它來負(fù)責(zé) suspend 所有 Java 線程括授。
原因很簡單,我們還有一步工作要做岩饼,就是要把該斷點觸發(fā)信息返回給調(diào)試器荚虚。如果我們先返回信息,然后 suspend 所有 Java 線程籍茧,這就無法保證在調(diào)試器收到信息時所有 Java 線程已經(jīng)被 suspend版述。
反之,先 Suspend 了所有 Java 線程寞冯,誰來負(fù)責(zé)發(fā)送信息給調(diào)試器呢渴析?
為了解決這個問題,我們通過 JDWP 的 EventDispatcher 線程來幫我們 suspend 線程和發(fā)送信息吮龄。實現(xiàn)的過程是俭茧,我們讓觸發(fā)斷點的 Java 線程來 PostEventSet(trace 中可以看到),把生成的 JDWP 事件放到一個隊列中漓帚,然后就開始等待母债。由 EventDispatcher 線程來負(fù)責(zé)從隊列中取出 JDWP 事件,并根據(jù)事件中的設(shè)定尝抖,來 suspend 所要求的 Java 線程并發(fā)送出該事件毡们。
在這里,我們在事件觸發(fā)的 Java 線程和 EventDispatcher 線程之間添加了一個同步機制昧辽,當(dāng)事件發(fā)送出去后漏隐,事件觸發(fā)的 Java 線程會把 JDWP 中的該事件刪除,到這里奴迅,整個 JDWP 事件處理就完成了
結(jié)語
我們在調(diào)試 Java 程序的時候,往往需要對虛擬機內(nèi)部的運行狀態(tài)進行觀察和調(diào)試,JDWP Agent 就充當(dāng)了調(diào)試器與 Java 虛擬機的溝通橋梁取具。它的工作原理簡單來說就是對于 JDWP 命令的處理和事件的管理脖隶。由于 JDWP 在 JPDA 中處于相對底層的位置,調(diào)試器發(fā)出一個 JDI 指令暇检,往往要通過很多 JDWP 命令來完成,
此外由于兩年前在做一個項目产阱,然后發(fā)現(xiàn)生產(chǎn)環(huán)境與本地開發(fā)環(huán)境一致的代碼,運行結(jié)果完全不一樣块仆,然后就通過Eclipse進行Remote遠程調(diào)代碼調(diào)試進行跟蹤构蹬,由此留下比較深刻的印象,看到此文章比較故進行整理悔据,隨便把IDEA和Eclipse的遠程調(diào)試截圖標(biāo)記一下庄敛。
來自文章