??????JDI屬于JPDA中最上層接口。定義了調(diào)試器(Debugger)所需要的一些調(diào)試接口?;谶@些接口,調(diào)試器可以及時(shí)地了解目標(biāo)虛擬機(jī)的狀態(tài),例如查看目標(biāo)虛擬機(jī)上有哪些類(lèi)和實(shí)例等。另外,調(diào)試者還可以控制目標(biāo)虛擬機(jī)的執(zhí)行,例如掛起和恢復(fù)目標(biāo)虛擬機(jī)上的線(xiàn)程,設(shè)置斷點(diǎn)等。
0、工作方式
??????首先,調(diào)試器(Debuuger)通過(guò) Bootstrap 獲取唯一的虛擬機(jī)管理器。
虛擬機(jī)管理器將在第一次被調(diào)用時(shí)初始化可用的鏈接器。一般地,調(diào)試器會(huì)默認(rèn)地采用啟動(dòng)型鏈接器進(jìn)行鏈接。
??????然后,調(diào)試器調(diào)用鏈接器的 launch () 來(lái)啟動(dòng)目標(biāo)程序,并完成調(diào)試器與目標(biāo)虛擬機(jī)的鏈接。
??????當(dāng)鏈接完成后,調(diào)試器與目標(biāo)虛擬機(jī)便可以進(jìn)行雙向通信了。調(diào)試器將用戶(hù)的操作轉(zhuǎn)化為調(diào)試命令,命令通過(guò)鏈接被發(fā)送到前端運(yùn)行目標(biāo)程序的虛擬機(jī)上;然后,目標(biāo)虛擬機(jī)根據(jù)接受的命令做出相應(yīng)的操作,將調(diào)試的結(jié)果發(fā)回給后端的調(diào)試器;最后,調(diào)試器可視化數(shù)據(jù)信息反饋給用戶(hù)。
1、模塊劃分
??????通過(guò)上面的描述,我們可以將jdi分成3部分: 數(shù)據(jù)模塊、連接模塊、事件處理模塊。
1.1 數(shù)據(jù)模塊
??????jdi的數(shù)據(jù)模塊,主要就是Mirror機(jī)制。Mirror 接口是JDI最底層的接口,JDI中幾乎所有其他接口都繼承于它。Mirror 機(jī)制是將目標(biāo)虛擬機(jī)上的所有數(shù)據(jù)、類(lèi)型、域、方法、事件、狀態(tài)和資源,以及調(diào)試器發(fā)向目標(biāo)虛擬機(jī)的事件請(qǐng)求等都映射成 Mirror 對(duì)象。
例如,在目標(biāo)虛擬機(jī)上,已裝載的類(lèi)被映射成 ReferenceType 鏡像,對(duì)象實(shí)例被映射成 ObjectReference 鏡像,基本類(lèi)型的值(如 float 等)被映射成 PrimitiveValue(如 FloatValue 等)。被調(diào)試的目標(biāo)程序的運(yùn)行狀態(tài)信息被映射到 StackFrame 鏡像中,在調(diào)試過(guò)程中所觸發(fā)的事件被映射成 Event 鏡像(如 StepEvent 等),調(diào)試器發(fā)出的事件請(qǐng)求被映射成 EventRequest 鏡像(如 StepRequest 等),被調(diào)試的目標(biāo)虛擬機(jī)則被映射成 VirtualMachine 鏡像。但是,JDI 并不保證目標(biāo)虛擬機(jī)上的每份信息和資源都只有唯一的鏡像與之對(duì)應(yīng),這是由 JDI 的具體實(shí)現(xiàn)所決定的。例如,目標(biāo)虛擬機(jī)上的某個(gè)事件有可能存在多個(gè) Event 鏡像與之對(duì)應(yīng),例如 BreakpointEvent 等。
??????Mirror 實(shí)例或是由調(diào)試器創(chuàng)建,或是由目標(biāo)虛擬機(jī)創(chuàng)建,調(diào)用 Mirror 實(shí)例 virtualMachine() 可以獲取其虛擬機(jī)信息。該接口提供了一套方法,可以用來(lái)直接或間接地獲取目標(biāo)虛擬機(jī)上所有的數(shù)據(jù)和狀態(tài)信息,也可以?huà)炱?、恢?fù)、終止目標(biāo)虛擬機(jī)。
1.2 連接模塊
??????連接是調(diào)試器與目標(biāo)虛擬機(jī)之間交互的渠道,一次連接可以由調(diào)試器發(fā)起,也可以由被調(diào)試的目標(biāo)虛擬機(jī)發(fā)起。一個(gè)調(diào)試器可以連接多個(gè)目標(biāo)虛擬機(jī),但一個(gè)目標(biāo)虛擬機(jī)最多只能連接一個(gè)調(diào)試器。下面的例子中就講了一種常見(jiàn)的連接方式: 由調(diào)試器啟動(dòng)目標(biāo)虛擬機(jī)的連接方式。也可以在虛擬機(jī)處于運(yùn)行狀態(tài)時(shí),采用attach的方式連接到目標(biāo)虛擬機(jī)(我們平時(shí)用的Intellij 用的就是這種方式)。
1.3 事件處理模塊
??????主要在com.sun.jdi.event 和 com.sun.jdi.request 包中。
-
事件類(lèi)型: JDI中事件的接口叫Event . 它定義了18種具體的事件類(lèi)型。
事件類(lèi)型
事件集是事件發(fā)送的最小單位,并且事件集一旦被創(chuàng)建,則不可以被修改。
事件請(qǐng)求:Event接口定義了request方法,該方法會(huì)返回由調(diào)試器Debugger發(fā)出的針對(duì)該事件的事件請(qǐng)求(EventRequest)。事件請(qǐng)求是由調(diào)試器向目標(biāo)虛擬機(jī)發(fā)出的,目的是請(qǐng)求目標(biāo)虛擬機(jī)在發(fā)生指定的事件后通知調(diào)試器。只有當(dāng)調(diào)試器發(fā)出的請(qǐng)求與目標(biāo)虛擬機(jī)上發(fā)生的事件匹配時(shí),這些事件才會(huì)被分發(fā)到各個(gè)事件集,進(jìn)而等待發(fā)送至調(diào)試器端。
??????當(dāng)然了,Debugger發(fā)送給Target VM的所有事件請(qǐng)求,不一定Target VM 都感興趣。因此JDI提供了事件的過(guò)濾機(jī)制,來(lái)刪選出最終真正要發(fā)送給Target VM的事件。
- 對(duì)事件請(qǐng)求的管理: 在JDI中,事件請(qǐng)求的管理是通過(guò)EventRequestManager來(lái)完成的。它有許多createXXXRequest方法來(lái)創(chuàng)建不同類(lèi)型的事件請(qǐng)求,也有許多deleteXXXRequest方法來(lái)刪除不同類(lèi)型的事件請(qǐng)求,還有xxxRequests方法來(lái)列出各種類(lèi)型的事件請(qǐng)求。有一點(diǎn)需要注意的是,這里由EventRequestManager創(chuàng)建的createXXXRequest的事件都是非激活的,因此這些事件請(qǐng)求當(dāng)發(fā)送給Target VM不會(huì)起任何作用,除非調(diào)用EventRequest的setEnable(true)使得該事件進(jìn)入激活狀態(tài)。
- 事件隊(duì)列:事件隊(duì)列(EventQueue)的擁有者是目標(biāo)虛擬機(jī),EventQueue 將這些事件集以“先進(jìn)先出”策略依次地發(fā)送到調(diào)試器端。EventQueue 負(fù)責(zé)管理來(lái)自目標(biāo)虛擬機(jī)的事件,一個(gè)被調(diào)試的目標(biāo)虛擬機(jī)上有且僅有一個(gè) EventQueue實(shí)例。
- Debugger 跟 targetVM之間的事件交互:
- Debugger調(diào)用Target VM的 eventQueue() 和 eventRequestManager() 分別獲取唯一的 EventQueue 實(shí)例和 EventRequestManager 實(shí)例.
- Debugger通過(guò) EventRequestManager 的 createXXXRequest() 創(chuàng)建需要的事件請(qǐng)求,并添加過(guò)濾器和設(shè)置掛起策略.
- targetVM 上某個(gè)事件觸發(fā)且匹配上eventRequest , 則將event放入對(duì)應(yīng)的eventSet.
- targetVM 上的EventQueue 管理這些eventSet, 按照FIFO原則發(fā)送給Debugger.
- Debugger通過(guò)第一步獲取到的EventQueue實(shí)例 獲取來(lái)自Target VM的事件響應(yīng)。
一句話(huà)概括就是 EventRequest總是由Debugger發(fā)向Target VM ,而當(dāng)請(qǐng)求與目標(biāo)虛擬機(jī)上發(fā)生事件匹配,則事件會(huì)被歸到EventSet中,EventSet會(huì)被Target VM的EventQueue所管理,并且按照FIFO原則發(fā)送到Debugger
2、一個(gè)例子
(1)首先來(lái)個(gè)測(cè)試
public class HelloWorld {
public static void main(String[] args) {
String str = "Hello world!";
System.out.println(str);
}
}
(2)JDI agent的例子
import java.util.List;
import java.util.Map;
import com.sun.jdi.*;
import com.sun.jdi.connect.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*;
/**
* Created by zhangpeng48 on 2018/7/16.
*/
public class MethodTrace {
private static VirtualMachine vm;
private static Process process;
private static EventRequestManager eventRequestManager;
private static EventQueue eventQueue;
private static EventSet eventSet;
private static boolean vmExit = false;
//write your own testclass
private static String className = "HelloWorld";
public static void main(String[] args) throws Exception {
System.out.println("begin....");
launchDebugee();
registerEvent();
processDebuggeeVM();
// Enter event loop
eventLoop();
destroyDebuggeeVM();
}
public static void launchDebugee() {
LaunchingConnector launchingConnector = Bootstrap
.virtualMachineManager().defaultConnector();
// Get arguments of the launching connector
Map<String, Connector.Argument> defaultArguments = launchingConnector
.defaultArguments();
Connector.Argument mainArg = defaultArguments.get("main");
Connector.Argument suspendArg = defaultArguments.get("suspend");
// Set class of main method
mainArg.setValue(className);
suspendArg.setValue("true");
try {
vm = launchingConnector.launch(defaultArguments);
} catch (Exception e) {
// ignore
}
}
public static void processDebuggeeVM() {
process = vm.process();
}
public static void destroyDebuggeeVM() {
process.destroy();
}
public static void registerEvent() {
// Register ClassPrepareRequest
eventRequestManager = vm.eventRequestManager();
MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest();
entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
entryReq.addClassFilter(className);
entryReq.enable();
MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();
exitReq.addClassFilter(className);
exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
exitReq.enable();
}
private static void eventLoop() throws Exception {
eventQueue = vm.eventQueue();
while (true) {
Thread.sleep(10000);
if (vmExit == true) {
System.out.println("vmexit");
break;
}
eventSet = eventQueue.remove();
EventIterator eventIterator = eventSet.eventIterator();
while (eventIterator.hasNext()) {
Event event = (Event) eventIterator.next();
execute(event);
if (!vmExit) {
eventSet.resume();
}
}
}
}
private static void execute(Event event) throws Exception {
if (event instanceof VMStartEvent) {
System.out.println("VM started");
} else if (event instanceof MethodEntryEvent) {
Method method = ((MethodEntryEvent) event).method();
System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());
System.out.printf("\t ReturnType:%s\n", method.returnTypeName());
} else if (event instanceof MethodExitEvent) {
Method method = ((MethodExitEvent) event).method();
System.out.printf("Exit -> method: %s\n",method.name());
} else if (event instanceof VMDisconnectEvent) {
vmExit = true;
}
}
}
(3)編譯與運(yùn)行
編譯測(cè)試 HelloWorld :
javac HelloWorld.java
編譯 JDI agent:
#注意 classpath 多個(gè)引用時(shí),在linux環(huán)境使用“:”分割。
javac -classpath $JAVA_HOME/lib/tools.jar:. MethodTrace.java
運(yùn)行:
java -classpath $JAVA_HOME/lib/tools.jar:. MethodTrace HelloWorld
結(jié)果是這個(gè)樣子的~

4)demo下載地址
鏈接: https://pan.baidu.com/s/1bKVKexvWKCh6F0hG4nkRDQ 密碼: etk3
3、核心api的分析
源碼地址:
hotspot實(shí)現(xiàn): hotspot的實(shí)現(xiàn)
JDK自帶實(shí)現(xiàn): openjdk 的實(shí)現(xiàn)
本文主要是對(duì)JDK自帶的實(shí)現(xiàn)進(jìn)行分析~
3.1 launchDebugee

首先獲取一個(gè)獲取虛擬機(jī)管理器virtualmachineManager, 然后獲取默認(rèn)的連接器
public List<LaunchingConnector> launchingConnectors() {
ArrayList var1 = new ArrayList(this.connectors.size());
Iterator var2 = this.connectors.iterator();
while(var2.hasNext()) {
Connector var3 = (Connector)var2.next();
if(var3 instanceof LaunchingConnector) {
var1.add((LaunchingConnector)var3);
}
}
return Collections.unmodifiableList(var1);
}
然后為連接器設(shè)置參數(shù) 主要是設(shè)置main(mainArg.setValue 設(shè)置該main對(duì)應(yīng)哪個(gè)類(lèi)), 跟suspend (suspend=y/n 是否在調(diào)試客戶(hù)端建立連接之后啟動(dòng) VM 。如果是y,則方便調(diào)試vm啟動(dòng)過(guò)程中的一些步驟。本文設(shè)置為true.)
最后 launch ( 啟動(dòng)目標(biāo)程序,連接調(diào)試器(Debuuger)與目標(biāo)虛擬機(jī)(VirtualMachine)) 。主要是創(chuàng)建了一個(gè)socket 以及一個(gè)VM . 代碼如下。
public VirtualMachine launch(Map<String, ? extends Argument> var1) throws IOException, IllegalConnectorArgumentsException, VMStartException {
// 獲取各種參數(shù)
String var3 = this.argument("home", var1).value();
String var4 = this.argument("options", var1).value();
String var5 = this.argument("main", var1).value();
boolean var6 = ((BooleanArgumentImpl)this.argument("suspend", var1)).booleanValue();
String var7 = this.argument("quote", var1).value();
String var8 = this.argument("vmexec", var1).value();
String var9 = null;
if(var7.length() > 1) {
throw new IllegalConnectorArgumentsException("Invalid length", "quote");
} else if(var4.indexOf("-Djava.compiler=") != -1 && var4.toLowerCase().indexOf("-djava.compiler=none") == -1) {
throw new IllegalConnectorArgumentsException("Cannot debug with a JIT compiler", "options");
} else {
// 進(jìn)入主題
ListenKey var10;
String var13;
if(!this.usingSharedMemory) {
// 本文分析的是SunCommandLineLauncher ,本默認(rèn)走這個(gè)分支。 下面函數(shù)的作用是綁定了一個(gè)socket 到 SocketTransportService
var10 = this.transportService().startListening();
} else {
Random var11 = new Random();
int var12 = 0;
while(true) {
try {
var13 = "javadebug" + String.valueOf(var11.nextInt(100000));
var10 = this.transportService().startListening(var13);
break;
} catch (IOException var18) {
++var12;
if(var12 > 5) {
throw var18;
}
}
}
}
// 獲取了socket的地址。
String var19 = var10.address();
// 創(chuàng)建了一個(gè)VM.
VirtualMachine var2;
try {
if(var3.length() > 0) {
var9 = var3 + File.separator + "bin" + File.separator + var8;
} else {
var9 = var8;
}
if(hasWhitespace(var9)) {
var9 = var7 + var9 + var7;
}
// 組裝參數(shù): transport , address, suspend
String var20 = "transport=" + this.transport().name() + ",address=" + var19 + ",suspend=" + (var6?'y':'n');
if(hasWhitespace(var20)) {
var20 = var7 + var20 + var7;
}
// 繼續(xù)組裝參數(shù) 組裝之后的結(jié)構(gòu)類(lèi)似: "java ${jrePath} -Xdebug -Xrunjdwp:transport=dt_socket,address=local:40023,suspend=y"
var13 = var9 + ' ' + var4 + ' ' + "-Xdebug " + "-Xrunjdwp:" + var20 + ' ' + var5;
// 核心。 launcher. VM的創(chuàng)建 下面重點(diǎn)分析一下。
var2 = this.launch(this.tokenizeCommand(var13, var7.charAt(0)), var19, var10, this.transportService());
} finally {
this.transportService().stopListening(var10);
}
return var2;
}
}
this.transportService().startListening(); 綁定了一個(gè)socket 到 SocketTransportService 。 分析如下:
ListenKey startListening(String var1, int var2) throws IOException {
InetSocketAddress var3;
if(var1 == null) {
// 創(chuàng)建InetSocketAddress 里面包含了ip 跟 port , 此處ip為 0.0.0.0 (Returns the InetAddress representing anyLocalAddress)
var3 = new InetSocketAddress(var2);
} else {
var3 = new InetSocketAddress(var1, var2);
}
// 創(chuàng)建socket
ServerSocket var4 = new ServerSocket();
// socket 的bind
var4.bind(var3);
return new SocketTransportService.SocketListenKey(var4);
}
this.launch(this.tokenizeCommand(var13, var7.charAt(0)), var19, var10, this.transportService()); 的實(shí)現(xiàn)
protected VirtualMachine launch(String[] var1, String var2, ListenKey var3, TransportService var4) throws IOException, VMStartException {
AbstractLauncher.Helper var5 = new AbstractLauncher.Helper(var1, var2, var3, var4);
var5.launchAndAccept(); // socket 執(zhí)行 accept ,connect
VirtualMachineManager var6 = Bootstrap.virtualMachineManager();
return var6.createVirtualMachine(var5.connection(), var5.process());// 建立一個(gè)virtualMachineManager
}
createVirtualMachine 中最核心的就是 創(chuàng)建了一個(gè) VirtualMachineImpl。
VirtualMachineImpl(VirtualMachineManager var1, Connection var2, Process var3, int var4) {
super((VirtualMachine)null);
this.vm = this;
this.vmManager = (VirtualMachineManagerImpl)var1;
this.process = var3;
this.sequenceNumber = var4;
this.threadGroupForJDI = new ThreadGroup(this.vmManager.mainGroupForJDI(), "JDI [" + this.hashCode() + "]");
// 創(chuàng)建一個(gè)TargetVM, 這個(gè)過(guò)程新建了一個(gè)線(xiàn)程,并且設(shè)置為后臺(tái)常駐。
this.target = new TargetVM(this, var2);
EventQueueImpl var5 = new EventQueueImpl(this, this.target);
// 新增一個(gè) event處理器 線(xiàn)程。
new InternalEventHandler(this, var5);
this.eventQueue = new EventQueueImpl(this, this.target);
// new一個(gè)事件請(qǐng)求管理器
this.eventRequestManager = new EventRequestManagerImpl(this);
// 起飛。。。
this.target.start();
IDSizes var6;
try {
var6 = IDSizes.process(this.vm);
} catch (JDWPException var8) {
throw var8.toJDIException();
}
// 下面的內(nèi)容可能跟mirror中提到的各種鏡像有關(guān)。。。
this.sizeofFieldRef = var6.fieldIDSize; // 屬性相關(guān)的鏡像
this.sizeofMethodRef = var6.methodIDSize; // 方法相關(guān)的鏡像
this.sizeofObjectRef = var6.objectIDSize; // 對(duì)象實(shí)例 相關(guān)的鏡像
this.sizeofClassRef = var6.referenceTypeIDSize; // 類(lèi)相關(guān)的鏡像
this.sizeofFrameRef = var6.frameIDSize; // StackFrame 鏡像
this.internalEventRequestManager = new EventRequestManagerImpl(this);
// createClassPrepareRequest 相關(guān)的event
ClassPrepareRequest var7 = this.internalEventRequestManager.createClassPrepareRequest();
var7.setSuspendPolicy(0);
var7.enable();
// createClassUnloadRequest 相關(guān)的event
ClassUnloadRequest var9 = this.internalEventRequestManager.createClassUnloadRequest();
var9.setSuspendPolicy(0);
var9.enable();
this.notifyInitCompletion();
}
當(dāng)連接完成后,調(diào)試器與目標(biāo)虛擬機(jī)便可以進(jìn)行雙向通信了。調(diào)試器將用戶(hù)的操作轉(zhuǎn)化為調(diào)試命令,命令通過(guò)連接被發(fā)送到前端運(yùn)行目標(biāo)程序的虛擬機(jī)上;然后,目標(biāo)虛擬機(jī)根據(jù)接受的命令做出相應(yīng)的操作,將調(diào)試的結(jié)果發(fā)回給后端的調(diào)試器;最后,調(diào)試器可視化數(shù)據(jù)信息反饋給用戶(hù)。
再補(bǔ)充個(gè)知識(shí)點(diǎn): 如果采用jdwp agent, 則建立完連接之后,第一件事情就是handshake 看一下代碼。
void handshake(Socket s, long timeout) throws IOException {
s.setSoTimeout((int)timeout);
byte[] hello = "JDWP-Handshake".getBytes("UTF-8");
s.getOutputStream().write(hello);
byte[] b = new byte[hello.length];
int received = 0;
while (received < hello.length) {
int n;
try {
n = s.getInputStream().read(b, received, hello.length-received);
} catch (SocketTimeoutException x) {
throw new IOException("handshake timeout");
}
if (n < 0) {
s.close();
throw new IOException("handshake failed - connection prematurally closed");
}
received += n;
}
for (int i=0; i<hello.length; i++) {
if (b[i] != hello[i]) {
throw new IOException("handshake failed - unrecognized message from target VM");
}
}
// disable read timeout
s.setSoTimeout(0);
發(fā)送的"JDWP-Handshake"就是JDWP協(xié)議里面規(guī)定的。在連接建立之后,發(fā)送數(shù)據(jù)包之前,debugger跟debuggee必須要有一個(gè)handshake的過(guò)程,handshake分為兩步,
- debugger發(fā)送14個(gè)字節(jié),也就是JDWP-Handshake,給debuggee;
- debuggee發(fā)送同樣的14個(gè)字節(jié)回應(yīng);
JDWP協(xié)議的細(xì)節(jié)將在另外一篇文章介紹。
3.2 registerEvent
注冊(cè)了2個(gè)event.
public void registerEvent() {
// Register ClassPrepareRequest 這個(gè)方法繼承了mirrorImpl, mirror機(jī)制也是我們后面分析的重點(diǎn)。
eventRequestManager = vm.eventRequestManager();
// 創(chuàng)建一個(gè)方法進(jìn)入的event
MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest();
entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
entryReq.addClassFilter(className);
entryReq.enable();
// 創(chuàng)建一個(gè)方法退出的event
MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();
exitReq.addClassFilter(className);
exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
exitReq.enable();
}
3.3 processDebuggeeVM
vm.process(). vm 跑起來(lái)。
3.4 eventLoop
事件循環(huán),具體處理在execute.
private void eventLoop() throws Exception {
eventQueue = vm.eventQueue();
while (true) {
Thread.sleep(10000);
if (vmExit == true) {
System.out.println("vmexit");
break;
}
eventSet = eventQueue.remove();
EventIterator eventIterator = eventSet.eventIterator();
while (eventIterator.hasNext()) {
Event event = (Event) eventIterator.next();
execute(event);
if (!vmExit) {
eventSet.resume();
}
}
}
}
execute: 目前只是簡(jiǎn)單地輸出了各種event的日志。
private void execute(Event event) throws Exception {
if (event instanceof VMStartEvent) {
System.out.println("VM started");
} else if (event instanceof MethodEntryEvent) {
Method method = ((MethodEntryEvent) event).method();
System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());
System.out.printf("\t ReturnType:%s\n", method.returnTypeName());
} else if (event instanceof MethodExitEvent) {
Method method = ((MethodExitEvent) event).method();
System.out.printf("Exit -> method: %s\n",method.name());
} else if (event instanceof VMDisconnectEvent) {
vmExit = true;
}
}
3.5 destroyDebuggeeVM
process.destroy();
參考文獻(xiàn)
1、JPDA 架構(gòu)研究20 - JDI的事件請(qǐng)求和處理模塊
2、https://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html?ca=drs-
3、https://yq.aliyun.com/articles/56?hmsr=toutiao.io&spm=5176.100240.searchblog.18&utm_medium=toutiao.io&utm_source=toutiao.io
