該文章來自於阿里巴巴技術協會(ATA)精選文章。java
Java調試概述
程序猿都調式或者debug過Java代碼吧?都體會過被PM,PD,測試,業務同窗們圍觀debug吧?說調試,先看看調試嚴格定義是什麼。引用Wikipedia定義:linux
調試(De-bug),又稱除錯,是發現和減小計算機程序或電子儀器設備中程序錯誤的一個過程。調試的基本步驟:
1. 發現程序錯誤的存在
2. 以隔離、消除的方式對錯誤進行定位
3. 肯定錯誤產生的緣由
4. 提出糾正錯誤的解決辦法
5. 對程序錯誤予以改正,從新測試程序員
用調試的好處是咱們就無需每次新測試都要從新編譯了,不用copy-paste一堆的System.out.println(很low但不少時候很管用有沒有?)。apache
更多時候咱們調試最直接簡單的辦法就是IDE,Java程序員用的最多的必然是Eclipse,Netbeans和IntelliJ也有各自忠實的粉絲,各有優劣。關於用IDE如何調試能夠另起一個話題再討論。
api
除了IDE以外,JDK也自帶了一些命令行調試工具也很方便。你們用的比較多的以下表所示:瀏覽器
命令 |
描述 |
jdb |
命令行調試工具 |
jps |
列出全部Java進程的PID |
jstack |
列出虛擬機進程的全部線程運行狀態 |
jmap |
列出堆內存上的對象狀態 |
jstat |
記錄虛擬機運行的狀態,監控性能 |
jconsole |
虛擬機性能/狀態檢查可視化工具 |
具體用法能夠參考JDK文檔,這些你們在線上調試應用的時候用的也很多,好比通常線上load高的問題排查步驟是安全
- 先用top找到耗資源的進程
- ps+grep找到對應的java進程/線程
- jstack分析哪些線程阻塞了,阻塞在哪裏
- jstat看看FullGC頻率
- jmap看看有沒有內存泄露
但這個也不是今天的重點,那麼問題來了(blue fly is the strongest):這些工具如何能獲取遠程Java進程的信息的?又是如何遠程控制Java進程的運行的? 相信有很多人和我同樣對這些工具的 實現原理 很好奇,本文就嘗試介紹下各中原因。bash
Java調試體系JPDA簡介
Java虛擬機設計了專門的API接口供調試和監控虛擬機使用,被稱爲Java平臺調試體系即Java Platform Debugger Architecture(JPDA)。JPDA按照抽象層次,又分爲三層,分別是服務器
- JVM TI - Java VM Tool Interface
- 虛擬機對外暴露的接口,包括debug和profile
- JDWP - Java Debug Wire Protocol
- JDI - Java Debug Interface
- Java庫接口,實現了JDWP協議的客戶端,調試器能夠用來和遠程被調試應用通訊
用一個不是特別準確可是比較容易理解的類比,你們能夠和HTTP作比較,能夠推斷他就是一個典型的C/S應用,因此也能夠很天然的想到,JDI是用TCP Socket和虛擬機通訊的,後面會詳細再介紹。
- IDE+JDI = 瀏覽器
- JDWP = HTTP
- JVMTI = RESTful接口
- Debugee虛擬機= REST服務端
和 其餘的Java模塊同樣,Java只定義了Spec規範,也提供了參考實現(Reference Implementation),可是第三方徹底能夠參照這個規範,按照本身的須要去實現其中任意一個組件,原則上除了規範上沒有定義的功能,他們應該能 正常的交互,好比Eclipse就沒有用Sun/Oracle的JDI,而是本身實現了一套(因爲開源license的兼容緣由),由於直接用JDWP協 議調用JVMTI是不會受GPL「污染」的。的確有第三方調試工具基於JVMTI作了一套調試工具,這樣效率更高,功能更豐富,由於JDI出於遠程調用的 安全考慮,作了一些功能的限制。用戶還能夠不用JDI,用本身熟悉的C或者腳本語言開發客戶端,遠程調試Java虛擬機,因此JPDA真個架構是很是靈活 的。
JVMTI
JVMTI是整個JPDA中最中要的API,也是虛擬機對外暴露的接口,掌握了JVMTI,你就能夠真正徹底掌控你的虛擬機,由於必須經過本地加載,因此暴露的豐富功能在安全上也沒有太大問題。更完整的API內容能夠參考JVMTI SPEC:
- 虛擬機信息
- 堆上的對象
- 線程和棧信息
- 全部的類信息
- 系統屬性,運行狀態
- 調試行爲
- 事件通知
在JPDA的這個圖裏,agent是其中很重要的一個模塊,正是他把JDI,JDWP,JVMTI三部分串聯成了一個總體。簡單來講agent的特性有
- C/C++實現的
- 被虛擬機以動態庫的方式加載
- 能調用本地JVMTI提供的調試能力
- 實現JDWP協議服務器端
- 與JDI(做爲客戶端)通訊(socket/shmem等方式)
Code speak louder than words. 上個代碼加註釋來解釋:
// Agent_OnLoad必須是入口函數,相似於main函數,規範規定
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
....
MethodTraceAgent* agent = new MethodTraceAgent();
agent->Init(vm);
agent->AddCapability();
agent->RegisterEvent();
...
}
/****** AddCapability(): init(): 初始化jvmti函數指針,全部功能的函數入口 *****/
jvmtiEnv* MethodTraceAgent::m_jvmti = 0;
jint ret = (vm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_0);
/****** AddCability(): 確認agent能訪問的虛擬機接口 *****/
jvmtiCapabilities caps;
memset(&caps, 0, sizeof(caps));
caps.can_generate_method_entry_events = 1;
// 設置當前環境
m_jvmti->AddCapabilities(&caps);
/****** RegisterEvent(): 建立一個新的回調函數 *****/
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;
// 設置回調函數
m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
// 開啓事件監聽
m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);
/****** HandleMethodEntry: 註冊的回調,獲取對應的信息 *****/
// 得到方法對應的類
m_jvmti->GetMethodDeclaringClass(method, &clazz);
// 得到類的簽名
m_jvmti->GetClassSignature(clazz, &signature, 0);
// 得到方法名字
m_jvmti->GetMethodName(method, &name, NULL, NULL);
寫好agent後,須要編譯,並在啓動Java進程時指定加載路徑
// 編譯動態連接庫
g++ -w -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libAgent.so
// 拷貝到 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/home/xiaoxia/lib
cp libAgent.so ~/lib
// 運行測試效果,記得load編譯的動態庫
javac MethodTraceTest.java
java -agentlib:Agent=first MethodTraceTest
Agent實現的動態連接庫其實有兩種加載方式:
- 虛擬機啓動初期加載 這個連接庫必須實現Agent_OnLoad做爲函數入口。這種方式能夠利用的接口和功能更多,由於他在被調式虛擬機運行的應用初始化以前就被調用了,可是限制是必須以顯示的參數指定啓動方式,這在線上環境上是不大現實的。
java -agentlib:<agent-lib-name>=<options> JavaClass
//Linux從LD_LIBRARY_PATH找so文件, Windows從PATH找該DLL文件。
java -agentpath:<path-to-agent>=<options> JavaClass
//直接從絕對路徑查找
- 動態加載 這是更靈活的方式,Java進程能夠正常啓動,若是須要,經過Sun/Orale提供的私有Attach API可 以連上對應的虛擬機,再經過JPDA方式控制,不過由於虛擬機已經開始運行了,因此功能上會有限制。咱們比較熟悉的jstack等jdk工具就是經過這種 方式作的,動態庫必須實現Agent_OnAttach做爲函數入口。若是有興趣理解Attach機制細節的話,能夠參考這個blog, 簡單來講,就是虛擬機默認起了一個線程(沒錯,就是jstack時看到Signal Dispatcher這貨),專門接受處理進程間singal通知,當他收到SIGQUIT時,就會啓動一個新的socket監聽線程(就是jstack 看到的Attach Listener線程)來接收命令,Attach Listener就是一個agent實現,他能處理不少dump命令,更重要的是他能再加載其餘agent,好比jdwp agent。
經過Attach機制,咱們能本身很是方便的實現一個jinfo或者其餘jdk tools,只需經過JPS獲取pid,在經過attach api去load咱們提供的agent,完整的jinfo例子也在附件裏。
import java.io.IOException;
import com.sun.tools.attach.VirtualMachine;
public class JInfo {
public static void main(String[] args) throws Exception {
String pid = args[0];
String agentName = "JInfoAgent";
System.out.printf("Atach to Pid %s, dynamic load agent %s \n", pid, agentName);
VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(pid);
virtualMachine.loadAgentLibrary(agentName, null);
virtualMachine.detach();
}
}
JDWP
JDWP 是 Java Debug Wire Protocol 的縮寫,它定義了調試器(debugger)和被調試的 Java 虛擬機(debugee)之間的通訊協議。他就是同過JVMTI Agent實現的,簡單來講,他就是對JVMTI調用(輸入和輸出,事件)的通訊定義。
JDWP 有兩種基本的包(packet)類型:命令包(command packet)和回覆包(reply packet)。JDWP 自己是無狀態的,所以對 命令出現的順序並不受限制。並且,JDWP 能夠是異步的,因此命令的發送方不須要等待接收到回覆就能夠繼續發送下一個命令。Debugger 和 Debugee 虛擬機都有可能發送命令:
- Debugger 經過發送命令獲取Debugee虛擬機的信息以及控制程序的執行。Debugger虛擬機經過發送 命令通知 Debugger 某些事件的發生,如到達斷點或是產生異常。
- 回覆是用來確認對應的命令是否執行成功(在包定義有一個flag字段對應),若是成功,回覆還有可能包含命令請求的數據,好比當前的線程信息或者變量的值。從 Debugee虛擬機發送的事件消息是不須要回復的。
下圖展現了一個可能的實現方式,再次強調下,Java的世界裏只定義了規範(Spec),不少實現細節能夠本身提供,好比虛擬機就有不少中實現(Sun HotSpot,IBM J9,Google Davik)。
通常咱們啓動遠程調試時,都會看到以下參數,其實表面了JDWP Agent就是經過啓動一個socket監聽來接受JDWP命令和發送事件信息的,並且,這個TCP鏈接能夠是雙向的:
// debugge是server先啓動監聽,ide是client發起鏈接
agentlib:jdwp=transport=dt_socket,server=y,address=8000
// debugger ide是server,經過JDI監聽,JDWP Agent做爲客戶端發起鏈接
agentlib:jdwp=transport=dt_socket,address=myhost:8000
JDI
JDI 屬於JPDA中最上層接口,也是Java程序員接觸的比較多的。他用起來也比較簡單,參考JDI的API Doc便可。全部的功能都和JVMTI提供的調試功能一一對應的(JVMTI還包括不少非調式接口,JDK5之前JVMTI是分爲JVMDI和JVMPI 的,分別對應調試debug和調優profile)。
仍是用一個例子來解釋最直接,你們能夠看到基本的流程都是相似的,真個JPDA調試的核心就是經過JVMTI的 調用 和事件 兩個方向的溝通實現的。
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.*;
public class MethodTrace {
private VirtualMachine vm;
private Process process;
private EventRequestManager eventRequestManager;
private EventQueue eventQueue;
private EventSet eventSet;
private boolean vmExit = false;
//write your own testclass
private String className = "MethodTraceTest";
public static void main(String[] args) throws Exception {
MethodTrace trace = new MethodTrace();
trace.launchDebugee();
trace.registerEvent();
trace.processDebuggeeVM();
// Enter event loop
trace.eventLoop();
trace.destroyDebuggeeVM();
}
public 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 void processDebuggeeVM() {
process = vm.process();
}
public void destroyDebuggeeVM() {
process.destroy();
}
public 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 void eventLoop() throws Exception {
eventQueue = vm.eventQueue();
while (true) {
if (vmExit == true) {
break;
}
eventSet = eventQueue.remove();
EventIterator eventIterator = eventSet.eventIterator();
while (eventIterator.hasNext()) {
Event event = (Event) eventIterator.next();
execute(event);
if (!vmExit) {
eventSet.resume();
}
}
}
}
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;
}
}
}
總結
整 個JDPA有很是清晰的分層,各司其職,讓整個調式過程簡單能夠擴展,而這一切其實都是構建在高司令巨牛逼的Java虛擬機抽象之上的,經過JVMTI將 抽象良好的虛擬機控制暴露出來,讓開發者能夠自由的掌控被調試的虛擬機。有興趣的同窗能夠運行下附近中的幾個例子,應該會有更充分的瞭解。
並且因爲規範的靈活性,若是有特殊需求,徹底能夠本身去從新實現和擴展,並且不限於Java,舉個例子,咱們能夠經過agent去加密解密加載的類,保護知識產權;咱們能夠記錄虛擬機運行過程,做爲自動化測試用例; 咱們還能夠把線上問題的診斷實踐自動化下來,作一個快速預判 ,爭取最寶貴的時間。
參考文檔