日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 >

Java调试那点事

發(fā)布時(shí)間:2025/4/5 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java调试那点事 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

該文章來自于阿里巴巴技術(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高的問題排查步驟是

  • 先用top找到耗資源的進(jìn)程
  • ps+grep找到對應(yīng)的java進(jìn)程/線程
  • jstack分析哪些線程阻塞了,阻塞在哪里
  • jstat看看FullGC頻率
  • jmap看看有沒有內(nèi)存泄露
  • 但這個(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í)的。
    java -agentlib:<agent-lib-name>=<options> JavaClass//Linux從LD_LIBRARY_PATH找so文件, Windows從PATH找該DLL文件。java -agentpath:<path-to-agent>=<options> JavaClass//直接從絕對路徑查找
    • 動態(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例子也在附件里。

    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 的縮寫,它定義了調(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:8000

    JDI

    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é)

    以上是生活随笔為你收集整理的Java调试那点事的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。