Java调试那点事
該文章來自于阿里巴巴技術(shù)協(xié)會(ATA)精選文章。
Java調(diào)試概述
程序猿都調(diào)式或者debug過Java代碼吧?都體會過被PM,PD,測試,業(yè)務(wù)同學(xué)們圍觀debug吧?說調(diào)試,先看看調(diào)試嚴(yán)格定義是什么。引用Wikipedia定義:
調(diào)試(De-bug),又稱除錯(cuò),是發(fā)現(xiàn)和減少計(jì)算機(jī)程序或電子儀器設(shè)備中程序錯(cuò)誤的一個(gè)過程。調(diào)試的基本步驟:
1. 發(fā)現(xiàn)程序錯(cuò)誤的存在
2. 以隔離、消除的方式對錯(cuò)誤進(jìn)行定位
3. 確定錯(cuò)誤產(chǎn)生的原因
4. 提出糾正錯(cuò)誤的解決辦法
5. 對程序錯(cuò)誤予以改正,重新測試
用調(diào)試的好處是我們就無需每次新測試都要重新編譯了,不用copy-paste一堆的System.out.println(很low但很多時(shí)候很管用有沒有?)。
更多時(shí)候我們調(diào)試最直接簡單的辦法就是IDE,Java程序員用的最多的必然是Eclipse,Netbeans和IntelliJ也有各自忠實(shí)的粉絲,各有優(yōu)劣。關(guān)于用IDE如何調(diào)試可以另起一個(gè)話題再討論。
除了IDE之外,JDK也自帶了一些命令行調(diào)試工具也很方便。大家用的比較多的如下表所示:
| jdb | 命令行調(diào)試工具 |
| jps | 列出所有Java進(jìn)程的PID |
| jstack | 列出虛擬機(jī)進(jìn)程的所有線程運(yùn)行狀態(tài) |
| jmap | 列出堆內(nèi)存上的對象狀態(tài) |
| jstat | 記錄虛擬機(jī)運(yùn)行的狀態(tài),監(jiān)控性能 |
| jconsole | 虛擬機(jī)性能/狀態(tài)檢查可視化工具 |
具體用法可以參考JDK文檔,這些大家在線上調(diào)試應(yīng)用的時(shí)候用的也不少,比如一般線上load高的問題排查步驟是
但這個(gè)也不是今天的重點(diǎn),那么問題來了(blue fly is the strongest):這些工具如何能獲取遠(yuǎn)程Java進(jìn)程的信息的?又是如何遠(yuǎn)程控制Java進(jìn)程的運(yùn)行的? 相信有不少人和我一樣對這些工具的 實(shí)現(xiàn)原理 很好奇,本文就嘗試介紹下各中緣由。
Java調(diào)試體系JPDA簡介
Java虛擬機(jī)設(shè)計(jì)了專門的API接口供調(diào)試和監(jiān)控虛擬機(jī)使用,被稱為Java平臺調(diào)試體系即Java Platform Debugger Architecture(JPDA)。JPDA按照抽象層次,又分為三層,分別是
- JVM TI - Java VM Tool Interface
- 虛擬機(jī)對外暴露的接口,包括debug和profile
- JDWP - Java Debug Wire Protocol
- 調(diào)試器和應(yīng)用之間通信的協(xié)議
- JDI - Java Debug Interface
- Java庫接口,實(shí)現(xiàn)了JDWP協(xié)議的客戶端,調(diào)試器可以用來和遠(yuǎn)程被調(diào)試應(yīng)用通信
用一個(gè)不是特別準(zhǔn)確但是比較容易理解的類比,大家可以和HTTP做比較,可以推斷他就是一個(gè)典型的C/S應(yīng)用,所以也可以很自然的想到,JDI是用TCP Socket和虛擬機(jī)通信的,后面會詳細(xì)再介紹。
- IDE+JDI = 瀏覽器
- JDWP = HTTP
- JVMTI = RESTful接口
- Debugee虛擬機(jī)= REST服務(wù)端
和其他的Java模塊一樣,Java只定義了Spec規(guī)范,也提供了參考實(shí)現(xiàn)(Reference Implementation),但是第三方完全可以參照這個(gè)規(guī)范,按照自己的需要去實(shí)現(xiàn)其中任意一個(gè)組件,原則上除了規(guī)范上沒有定義的功能,他們應(yīng)該能正常的交互,比如Eclipse就沒有用Sun/Oracle的JDI,而是自己實(shí)現(xiàn)了一套(由于開源license的兼容原因),因?yàn)橹苯佑肑DWP協(xié)議調(diào)用JVMTI是不會受GPL“污染”的。的確有第三方調(diào)試工具基于JVMTI做了一套調(diào)試工具,這樣效率更高,功能更豐富,因?yàn)镴DI出于遠(yuǎn)程調(diào)用的安全考慮,做了一些功能的限制。用戶還可以不用JDI,用自己熟悉的C或者腳本語言開發(fā)客戶端,遠(yuǎn)程調(diào)試Java虛擬機(jī),所以JPDA真?zhèn)€架構(gòu)是非常靈活的。
JVMTI
JVMTI是整個(gè)JPDA中最中要的API,也是虛擬機(jī)對外暴露的接口,掌握了JVMTI,你就可以真正完全掌控你的虛擬機(jī),因?yàn)楸仨毻ㄟ^本地加載,所以暴露的豐富功能在安全上也沒有太大問題。更完整的API內(nèi)容可以參考JVMTI SPEC:
- 虛擬機(jī)信息
- 堆上的對象
- 線程和棧信息
- 所有的類信息
- 系統(tǒng)屬性,運(yùn)行狀態(tài)
- 調(diào)試行為
- 設(shè)置斷點(diǎn)
- 掛起現(xiàn)場
- 調(diào)用方法
- 事件通知
- 斷點(diǎn)發(fā)生
- 異步調(diào)用
在JPDA的這個(gè)圖里,agent是其中很重要的一個(gè)模塊,正是他把JDI,JDWP,JVMTI三部分串聯(lián)成了一個(gè)整體。簡單來說agent的特性有
- C/C++實(shí)現(xiàn)的
- 被虛擬機(jī)以動態(tài)庫的方式加載
- 能調(diào)用本地JVMTI提供的調(diào)試能力
- 實(shí)現(xiàn)JDWP協(xié)議服務(wù)器端
- 與JDI(作為客戶端)通信(socket/shmem等方式)
Code speak louder than words. 上個(gè)代碼加注釋來解釋:
// Agent_OnLoad必須是入口函數(shù),類似于main函數(shù),規(guī)范規(guī)定 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函數(shù)指針,所有功能的函數(shù)入口 *****/jvmtiEnv* MethodTraceAgent::m_jvmti = 0;jint ret = (vm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_0);/****** AddCability(): 確認(rèn)agent能訪問的虛擬機(jī)接口 *****/jvmtiCapabilities caps;memset(&caps, 0, sizeof(caps));caps.can_generate_method_entry_events = 1;// 設(shè)置當(dāng)前環(huán)境m_jvmti->AddCapabilities(&caps);/****** RegisterEvent(): 創(chuàng)建一個(gè)新的回調(diào)函數(shù) *****/ jvmtiEventCallbacks callbacks;memset(&callbacks, 0, sizeof(callbacks));callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;// 設(shè)置回調(diào)函數(shù)m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));// 開啟事件監(jiān)聽m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);/****** HandleMethodEntry: 注冊的回調(diào),獲取對應(yīng)的信息 *****/// 獲得方法對應(yīng)的類m_jvmti->GetMethodDeclaringClass(method, &clazz);// 獲得類的簽名m_jvmti->GetClassSignature(clazz, &signature, 0);// 獲得方法名字m_jvmti->GetMethodName(method, &name, NULL, NULL);寫好agent后,需要編譯,并在啟動Java進(jìn)程時(shí)指定加載路徑
// 編譯動態(tài)鏈接庫 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// 運(yùn)行測試效果,記得load編譯的動態(tài)庫 javac MethodTraceTest.java java -agentlib:Agent=first MethodTraceTest
Agent實(shí)現(xiàn)的動態(tài)鏈接庫其實(shí)有兩種加載方式:
- 虛擬機(jī)啟動初期加載 這個(gè)鏈接庫必須實(shí)現(xiàn)Agent_OnLoad作為函數(shù)入口。這種方式可以利用的接口和功能更多,因?yàn)樗诒徽{(diào)式虛擬機(jī)運(yùn)行的應(yīng)用初始化之前就被調(diào)用了,但是限制是必須以顯示的參數(shù)指定啟動方式,這在線上環(huán)境上是不大現(xiàn)實(shí)的。
- 動態(tài)加載 這是更靈活的方式,Java進(jìn)程可以正常啟動,如果需要,通過Sun/Orale提供的私有Attach API可以連上對應(yīng)的虛擬機(jī),再通過JPDA方式控制,不過因?yàn)樘摂M機(jī)已經(jīng)開始運(yùn)行了,所以功能上會有限制。我們比較熟悉的jstack等jdk工具就是通過這種方式做的,動態(tài)庫必須實(shí)現(xiàn)Agent_OnAttach作為函數(shù)入口。如果有興趣理解Attach機(jī)制細(xì)節(jié)的話,可以參考這個(gè)blog,簡單來說,就是虛擬機(jī)默認(rèn)起了一個(gè)線程(沒錯(cuò),就是jstack時(shí)看到Signal Dispatcher這貨),專門接受處理進(jìn)程間singal通知,當(dāng)他收到SIGQUIT時(shí),就會啟動一個(gè)新的socket監(jiān)聽線程(就是jstack看到的Attach Listener線程)來接收命令,Attach Listener就是一個(gè)agent實(shí)現(xiàn),他能處理很多dump命令,更重要的是他能再加載其他agent,比如jdwp agent。
通過Attach機(jī)制,我們能自己非常方便的實(shí)現(xiàn)一個(gè)jinfo或者其他jdk tools,只需通過JPS獲取pid,在通過attach api去load我們提供的agent,完整的jinfo例子也在附件里。
JDWP
JDWP 是 Java Debug Wire Protocol 的縮寫,它定義了調(diào)試器(debugger)和被調(diào)試的 Java 虛擬機(jī)(debugee)之間的通信協(xié)議。他就是同過JVMTI Agent實(shí)現(xiàn)的,簡單來說,他就是對JVMTI調(diào)用(輸入和輸出,事件)的通信定義。
JDWP 有兩種基本的包(packet)類型:命令包(command packet)和回復(fù)包(reply packet)。JDWP 本身是無狀態(tài)的,因此對 命令出現(xiàn)的順序并不受限制。而且,JDWP 可以是異步的,所以命令的發(fā)送方不需要等待接收到回復(fù)就可以繼續(xù)發(fā)送下一個(gè)命令。Debugger 和 Debugee 虛擬機(jī)都有可能發(fā)送命令:
- Debugger 通過發(fā)送命令獲取Debugee虛擬機(jī)的信息以及控制程序的執(zhí)行。Debugger虛擬機(jī)通過發(fā)送 命令通知 Debugger 某些事件的發(fā)生,如到達(dá)斷點(diǎn)或是產(chǎn)生異常。
- 回復(fù)是用來確認(rèn)對應(yīng)的命令是否執(zhí)行成功(在包定義有一個(gè)flag字段對應(yīng)),如果成功,回復(fù)還有可能包含命令請求的數(shù)據(jù),比如當(dāng)前的線程信息或者變量的值。從 Debugee虛擬機(jī)發(fā)送的事件消息是不需要回復(fù)的。
下圖展示了一個(gè)可能的實(shí)現(xiàn)方式,再次強(qiáng)調(diào)下,Java的世界里只定義了規(guī)范(Spec),很多實(shí)現(xiàn)細(xì)節(jié)可以自己提供,比如虛擬機(jī)就有很多中實(shí)現(xiàn)(Sun HotSpot,IBM J9,Google Davik)。
一般我們啟動遠(yuǎn)程調(diào)試時(shí),都會看到如下參數(shù),其實(shí)表面了JDWP Agent就是通過啟動一個(gè)socket監(jiān)聽來接受JDWP命令和發(fā)送事件信息的,而且,這個(gè)TCP連接可以是雙向的:
// debugge是server先啟動監(jiān)聽,ide是client發(fā)起連接 agentlib:jdwp=transport=dt_socket,server=y,address=8000// debugger ide是server,通過JDI監(jiān)聽,JDWP Agent作為客戶端發(fā)起連接 agentlib:jdwp=transport=dt_socket,address=myhost:8000JDI
JDI屬于JPDA中最上層接口,也是Java程序員接觸的比較多的。他用起來也比較簡單,參考JDI的API Doc即可。所有的功能都和JVMTI提供的調(diào)試功能一一對應(yīng)的(JVMTI還包括很多非調(diào)式接口,JDK5以前JVMTI是分為JVMDI和JVMPI的,分別對應(yīng)調(diào)試debug和調(diào)優(yōu)profile)。
還是用一個(gè)例子來解釋最直接,大家可以看到基本的流程都是類似的,真?zhèn)€JPDA調(diào)試的核心就是通過JVMTI的?調(diào)用?和事件?兩個(gè)方向的溝通實(shí)現(xiàn)的。
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 testclassprivate String className = "MethodTraceTest";public static void main(String[] args) throws Exception {MethodTrace trace = new MethodTrace();trace.launchDebugee();trace.registerEvent();trace.processDebuggeeVM();// Enter event looptrace.eventLoop();trace.destroyDebuggeeVM();}public void launchDebugee() {LaunchingConnector launchingConnector = Bootstrap.virtualMachineManager().defaultConnector();// Get arguments of the launching connectorMap<String, Connector.Argument> defaultArguments = launchingConnector.defaultArguments();Connector.Argument mainArg = defaultArguments.get("main");Connector.Argument suspendArg = defaultArguments.get("suspend");// Set class of main methodmainArg.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 ClassPrepareRequesteventRequestManager = 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;}} }總結(jié)
整個(gè)JDPA有非常清晰的分層,各司其職,讓整個(gè)調(diào)式過程簡單可以擴(kuò)展,而這一切其實(shí)都是構(gòu)建在高司令巨牛逼的Java虛擬機(jī)抽象之上的,通過JVMTI將抽象良好的虛擬機(jī)控制暴露出來,讓開發(fā)者可以自由的掌控被調(diào)試的虛擬機(jī)。有興趣的同學(xué)可以運(yùn)行下附近中的幾個(gè)例子,應(yīng)該會有更充分的了解。
而且由于規(guī)范的靈活性,如果有特殊需求,完全可以自己去重新實(shí)現(xiàn)和擴(kuò)展,而且不限于Java,舉個(gè)例子,我們可以通過agent去加密解密加載的類,保護(hù)知識產(chǎn)權(quán);我們可以記錄虛擬機(jī)運(yùn)行過程,作為自動化測試用例;?我們還可以把線上問題的診斷實(shí)踐自動化下來,做一個(gè)快速預(yù)判?,爭取最寶貴的時(shí)間。
總結(jié)
- 上一篇: Android 依赖注入可以更简单 ——
- 下一篇: Java基于自定义注解的面向切面的实现