Debug 的時(shí)候,都遇到過(guò)手速太快,直接跳過(guò)了自己想調(diào)試的方法、代碼的時(shí)候吧……
一旦跳過(guò),可能就得重新執(zhí)行一遍,準(zhǔn)備數(shù)據(jù)、重新啟動(dòng)可能幾分鐘就過(guò)去了。
好在IDE 們都很強(qiáng)大,還給你后悔的機(jī)會(huì),可以直接刪除某個(gè) Stack Frame,直接返回到之前的狀態(tài),確切的說(shuō)是返回到之前的某個(gè) Stack Frame,從而實(shí)現(xiàn)讓程序“逆向運(yùn)行”。
這個(gè) Reset Frame 的能力,可不只是返回上一步,上 N 步也是可以的;選中你期望的那個(gè)幀,直接Reset Frame/Drop Frame,可以直接回到調(diào)用棧上的某個(gè)棧幀,時(shí)間反轉(zhuǎn)!
可惜這玩意也不是那么萬(wàn)能,畢竟是通過(guò) stack pop 這種操作實(shí)現(xiàn),實(shí)際上只是給調(diào)用棧棧頂?shù)?N 個(gè) frame pop 出來(lái)而已,還談不上是真正的“反向 DEBUG”。
相比之下, GDB 的 Reverse Debugging 就比較強(qiáng)大,真正的 “反向” DEBUG,逆向運(yùn)行,實(shí)現(xiàn)回放。
所以吧在運(yùn)行過(guò)程中,已經(jīng)修改的數(shù)據(jù),比如引用傳遞的方法參數(shù)、變量,一旦修改肯定回退不了,不然真的成時(shí)光機(jī)了。
這些亂七八糟的調(diào)試功能,都是基于 Java 內(nèi)置的 Debug 體系來(lái)實(shí)現(xiàn)的。
JAVA DEBUG 體系
Java 提供了一個(gè)完整的 Debug 體系 JPDA (Java Platform Debugger Architecture),這個(gè) JPDA 架構(gòu)體系由 3 部分組成:
JVM TI - Java VM Tool Interface
JDWP - Java Debug Wire Protocol
JDI - Java Debug Interface
如果結(jié)合IDE 來(lái)看,那么一個(gè)完整的 Debug 功能看起來(lái)就是這個(gè)樣子:
解釋一下這個(gè)體系:
JVM TI 是一個(gè) JVM 提供的一個(gè)調(diào)試接口,提供了一系列控制 JVM 行為的功能,比如分析、調(diào)試、監(jiān)控、線程分析等等。也就是說(shuō),這個(gè)接口定義了一系列調(diào)試分析功能,而 JVM 實(shí)現(xiàn)了這個(gè)接口,從而提供調(diào)試能力。
不過(guò)吧,這個(gè)接口畢竟是 C++的,調(diào)用起來(lái)確實(shí)不方便,所以Java 還提供了 JDI 這么個(gè) Java 接口。
JDI 接口使用 JDWP 這個(gè)私有的應(yīng)用層協(xié)議,通過(guò) TCP 和目標(biāo) VM 的 JVMTI 接口進(jìn)行交互。
也可以把簡(jiǎn)單這個(gè) JDWP 協(xié)議理解為 JSF/Dubbo 協(xié)議;相當(dāng)于 IDE 里通過(guò) JDI 這個(gè) SDK,使用 JDWP 協(xié)議調(diào)用遠(yuǎn)程 JVMTI 的 RPC 接口,來(lái)傳輸調(diào)試時(shí)的各種斷點(diǎn)、查看操作。
可能有人會(huì)問(wèn),搞什么套殼!要什么 JDWP,我直接 JVMTI 調(diào)試不是更香,鏈路越短性能越高!
當(dāng)然可以,比如 Arthas 里的部分功能,就直接使用了 JVMTI 接口,要什么 JDI!直接 JVMTI 干就完了。
開(kāi)個(gè)玩笑,Arthas 畢竟不是 Debug 工具,人家根本就不用 JDI 接口。而且 JVMTI 的能力也不只是斷點(diǎn),它的功能非常多:
左邊的功能類(lèi),提供了各種亂七八糟的功能,比如我們常用的添加一個(gè)斷點(diǎn):
jvmtiError SetBreakpoint(jvmtiEnv* env, jmethodID method, jlocation location)
右邊的事件類(lèi),可以簡(jiǎn)單的理解為回調(diào);還是拿斷點(diǎn)舉例,如果我用上面的 SetBreakpoint 添加了一個(gè)斷點(diǎn),那么當(dāng)執(zhí)行到該位置時(shí),就會(huì)觸發(fā)這個(gè)事件:
void JNICALL Breakpoint(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jmethodID method, jlocation location)
JVMTI 的功能非常之多,而 JDI 只是實(shí)現(xiàn)了部分 JVMTI 的方法,所以某些專(zhuān)業(yè)的 Profiler 工具,可能會(huì)直接使用 JVMTI,從而實(shí)現(xiàn)更豐富的診斷分析功能。
遠(yuǎn)程調(diào)試與本地調(diào)試
不知道大家有沒(méi)有留意過(guò)本地 Debug 啟動(dòng)時(shí)的日志:
第一行是隱藏了后半段的啟動(dòng)命令,展開(kāi)后是這個(gè)樣子:
/path/to/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631,suspend=y,server=n -javaagent:/path/to/jetbrains/debugger-agent.jar ...
第二行是一個(gè) Connected 日志,意思是使用 socket 連接到遠(yuǎn)程 VM 的53631端口
上一段說(shuō)到,IDE 通過(guò) JDI 接口,使用 JDWP 協(xié)議和目標(biāo) VM 的 JVMTI 交互。這里的 53631 端口,就是目標(biāo) JVM 暴露出的 JVM TI 的 server 端口。
而第一行里,IDEA 自動(dòng)給我們加上了 -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631 這么一段,這個(gè)參數(shù)的意思就是,讓 jvm 以 53631 暴露 jdwp 協(xié)議
小知識(shí),這個(gè) agentlib 可不只是為 jvmti 提供的。它還可以讓 JVM 加載其他的 native lib包,直接“外掛”到你的 jvm 上,下面是“外掛”的參數(shù)格式:
所以吧,上面的描述其實(shí)不太嚴(yán)謹(jǐn),更專(zhuān)業(yè)的說(shuō)法是:
讓 JVM 加載 JDWP 這個(gè) agent 庫(kù),參數(shù)為transport=dt_socket,address=127.0.0.1:53631 ,這個(gè) jdwp agent 庫(kù)以 53631 端口提供了 jdwp 協(xié)議的 server。只不過(guò)這個(gè) jdwp 是jvm 內(nèi)部的庫(kù),不需要額外的 so/dylib/dll 文件。
如有需要,你完全可以弄個(gè) “datupiao” 的 agentlib,“外掛”到這個(gè) jvm 上,然后在這個(gè) lib 里調(diào)用 JVMTI 接口,然后暴露個(gè)端口提供服務(wù)和遠(yuǎn)程交互,實(shí)現(xiàn)自己的 jdwp!
可能某些老板們注意到了,本地調(diào)試還要127.0.0.1走tcp 交互一遍,那遠(yuǎn)程調(diào)試呢?
基于上面的解釋?zhuān)镜卣{(diào)試和遠(yuǎn)程調(diào)試真的沒(méi)啥區(qū)別!或者說(shuō),在目前 IDEA/Eclipse 的實(shí)現(xiàn)下,不存在本地調(diào)試,都是遠(yuǎn)程!只不過(guò)一個(gè)是 127.0.0.1,一個(gè)是遠(yuǎn)程的 IP 而已。
在本地調(diào)試時(shí),IDEA 會(huì)自動(dòng)給我們的 JVM 增加 agent 參數(shù),隨機(jī)指定一個(gè)端口,然后通過(guò) JDI 接口連接,代碼大概長(zhǎng)這樣(JDI 的 SDK 在 JDK_HOME/lib/tools.jar ):
Map env = connector.defaultArguments(); env.get("hostname").setValue(hostname); env.get("port").setValue(port); VirtualMachine vm = connector.attach(env);
瞅瞅, VirtualMachine 里的就這點(diǎn)方法,能力上比 JVMTI 還是差遠(yuǎn)了
List classesByName(String className); List allClasses(); void redefineClasses(Map? extends ReferenceType, byte[]?> classToBytes); List allThreads(); void suspend(); void resume(); List topLevelThreadGroups(); EventQueue eventQueue(); EventRequestManager eventRequestManager(); VoidValue mirrorOfVoid(); Process process();
再回來(lái)看看 IDEA 中獨(dú)立的遠(yuǎn)程調(diào)試,配置好之后,紅框里的信息會(huì)提示你 ,遠(yuǎn)程的 JVM 需增加這一段啟動(dòng)參數(shù),而且支持多個(gè)版本 JDK 的格式,CV 大法就能直接用。
-agentlib 和 -javaagent
有些細(xì)心的同學(xué)可能發(fā)現(xiàn)了,IDEA 默認(rèn)的啟動(dòng)腳本里,同時(shí)配置了 -agentlib 和 -javaagent。
-javaagent:/path/to/jetbrains/debugger-agent.jar
這個(gè) debugger-agent吧,其實(shí)也沒(méi)干啥事,只是對(duì) JDK 內(nèi)置的一些線程做了些增強(qiáng),輔助 IDEA 的 debug 功能,支持一些異步的調(diào)試。
agentlib、javaagent 這倆兄弟,定位其實(shí)很像,都是加載自定義的代碼。
不過(guò)區(qū)別在于,agentlib 是加載 native lib,需要c/cpp 去寫(xiě),相當(dāng)于外掛自己的代碼在 jvm 上,可以為所欲為,比如在 agentlib 里調(diào)用上面說(shuō)的 JVMTI 。
而 javaagent 是用 java 寫(xiě)的,可以直接用上層的 Instrumentation API,做一些類(lèi)的增強(qiáng)轉(zhuǎn)換之類(lèi),這也是大多數(shù) APM Agent、Profiler Agent實(shí)現(xiàn)的基本原理。
Arthas 的玩法
Arthas 的核心入口,其實(shí)還是 javaagent,支持靜態(tài)加載和動(dòng)態(tài)加載兩種玩法。
靜態(tài)沒(méi)啥好說(shuō)的,啟動(dòng)腳本里增加一個(gè)-javaagent:/tmp/test/arthas-agent.jar,然后為所欲為。
動(dòng)態(tài)的叫 attach,使用 Java 提供的 VirtualMachine 就可以實(shí)現(xiàn)運(yùn)行時(shí)添加 -javaagent,效果一樣:
VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor); virtualMachine.loadAgent(agentPath, agentArgs);
這個(gè) Agent 在 JVM 里啟動(dòng)了一個(gè)TCP server,用于收發(fā) Arthas Client 的各種 trace、watch 、Dashboard 等指令,然后通過(guò) Instrumentation 增強(qiáng)Class 插入代碼、或者直接調(diào)用某些 Java API,實(shí)現(xiàn)各種功能。
注意到了嗎?Arthas 可以直接下載一個(gè) jar 包,java -jar 就能連上。
其實(shí)吧,它這個(gè)直接啟動(dòng)的 jar 包,是一個(gè) boot 包,啟動(dòng)之后把亂七八糟的 jar 都下載下來(lái)。接著動(dòng)態(tài) attach 的方式,連接到本機(jī)指定進(jìn)程號(hào)的 JVM,然后再為所欲為。
在 3.5 版本之后,Arthas 還新增了一個(gè) vmtool 命令,這個(gè)命令可以直接獲取內(nèi)存中的指定對(duì)象實(shí)例。
$ vmtool --action getInstances --className java.lang.String --limit 10 @String[][ @String[com/taobao/arthas/core/shell/session/Session], @String[com.taobao.arthas.core.shell.session.Session], @String[com/taobao/arthas/core/shell/session/Session], @String[com/taobao/arthas/core/shell/session/Session], @String[com/taobao/arthas/core/shell/session/Session.class], @String[com/taobao/arthas/core/shell/session/Session.class], @String[com/taobao/arthas/core/shell/session/Session.class], @String[com/], @String[java/util/concurrent/ConcurrentHashMap$ValueIterator], @String[java/util/concurrent/locks/LockSupport], ]
直接獲取內(nèi)存對(duì)象,這玩意只靠 Instrumentation API 可做不到。Arthas 搞了個(gè)騷操作,直接 JNI 調(diào)用自定義 lib,用過(guò) cpp 直接調(diào)用了 JVMTI 的 API,融合了 Instrumentation 和 JVMTI 的能力,這下是真的為所欲為了!
#include #include #include #include #include "arthas_VmTool.h" // under target/native/javah/ static jvmtiEnv *jvmti; ... extern "C" JNIEXPORT jobjectArray JNICALL Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) { jlong tag = getTag(); limitCounter.init(limit); jvmtiError error = jvmti->IterateOverInstancesOfClass(klass, JVMTI_HEAP_OBJECT_EITHER, HeapObjectCallback, &tag); if (error) { printf("ERROR: JVMTI IterateOverInstancesOfClass failed!%un", error); return NULL; } jint count = 0; jobject *instances; error = jvmti->GetObjectsWithTags(1, &tag, &count, &instances, NULL); if (error) { printf("ERROR: JVMTI GetObjectsWithTags failed!%un", error); return NULL; } jobjectArray array = env->NewObjectArray(count, klass, NULL); //添加元素到數(shù)組 for (int i = 0; i < count; i++) { env-?>SetObjectArrayElement(array, i, instances[i]); } jvmti->Deallocate(reinterpret_cast(instances)); return array; }
總結(jié)
Debug 基于 JDPA 體系
IDE 直接接入 JDPA 體系中的 JDI 接口完成
JDI 通過(guò) JDWP 協(xié)議,調(diào)用遠(yuǎn)程 VM 的 JVMTI 接口
JDWP 是通過(guò) agentlib 加載的,agentlib 算是一個(gè) native 的靜態(tài)“外掛”接口
javaagent 是 JAVA 層面的“外掛”接口,用過(guò) Instrumentation API(Java)實(shí)現(xiàn)各種功能,主要用于APM、Profiler 工具
如果你想,在 javaagent 里調(diào)用功能更豐富的 JVMTI 也不是不行。
審核編輯 黃宇
-
JAVA
+關(guān)注
關(guān)注
19文章
2960瀏覽量
104565 -
IDE
+關(guān)注
關(guān)注
0文章
335瀏覽量
46681 -
DEBUG
+關(guān)注
關(guān)注
3文章
90瀏覽量
19889
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論