Debug 的時候,都遇到過手速太快,直接跳過了自己想除錯的方法、程式碼的時候吧……
一旦跳過,可能就得重新執行一遍,準備資料、重新啟動可能幾分鐘就過去了。
好在IDE 們都很強大,還給你後悔的機會,可以直接刪除某個 Stack Frame,直接返回到之前的狀態,確切的說是返回到之前的某個 Stack Frame,從而實現讓程式“逆向執行”。
這個 Reset Frame 的能力,可不只是返回上一步,上 N 步也是可以的;選中你期望的那個幀,直接Reset Frame/Drop Frame,可以直接回到呼叫棧上的某個棧幀,時間反轉!
可惜這玩意也不是那麼萬能,畢竟是透過 stack pop 這種操作實現,實際上只是給呼叫棧棧頂的 N 個 frame pop 出來而已,還談不上是真正的“反向 DEBUG”。
相比之下, GDB 的 Reverse Debugging 就比較強大,真正的 “反向” DEBUG,逆向執行,實現回放。
所以吧在執行過程中,已經修改的資料,比如引用傳遞的方法引數、變數,一旦修改肯定回退不了,不然真的成時光機了。
這些亂七八糟的除錯功能,都是基於 Java 內建的 Debug 體系來實現的。
JAVA DEBUG 體系
Java 提供了一個完整的 Debug 體系 JPDA (Java Platform Debugger Architecture),這個 JPDA 架構體系由 3 部分組成:
-
JVM TI - Java VM Tool Interface
-
JDWP - Java Debug Wire Protocol
-
JDI - Java Debug Interface
如果結合IDE 來看,那麼一個完整的 Debug 功能看起來就是這個樣子:
解釋一下這個體系:
JVM TI 是一個 JVM 提供的一個除錯介面,提供了一系列控制 JVM 行為的功能,比如分析、除錯、監控、執行緒分析等等。也就是說,這個介面定義了一系列除錯分析功能,而 JVM 實現了這個介面,從而提供除錯能力。
不過吧,這個介面畢竟是 C++的,呼叫起來確實不方便,所以Java 還提供了 JDI 這麼個 Java 介面。
JDI 介面使用 JDWP 這個私有的應用層協議,透過 TCP 和目標 VM 的 JVMTI 介面進行互動。
也可以把簡單這個 JDWP 協議理解為 JSF/Dubbo 協議;相當於 IDE 裡透過 JDI 這個 SDK,使用 JDWP 協議呼叫遠端 JVMTI 的 RPC 介面,來傳輸除錯時的各種斷點、檢視操作。
可能有人會問,搞什麼套殼!要什麼 JDWP,我直接 JVMTI 除錯不是更香,鏈路越短效能越高!
當然可以,比如 Arthas 裡的部分功能,就直接使用了 JVMTI 介面,要什麼 JDI!直接 JVMTI 幹就完了。
開個玩笑,Arthas 畢竟不是 Debug 工具,人家根本就不用 JDI 介面。而且 JVMTI 的能力也不只是斷點,它的功能非常多:
左邊的功能類,提供了各種亂七八糟的功能,比如我們常用的新增一個斷點:
jvmtiError
SetBreakpoint(jvmtiEnv* env,
jmethodID method,
jlocation location)
右邊的事件類,可以簡單的理解為回撥;還是拿斷點舉例,如果我用上面的 SetBreakpoint 新增了一個斷點,那麼當執行到該位置時,就會觸發這個事件:
void JNICALL
Breakpoint(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jmethodID method,
jlocation location)
JVMTI 的功能非常之多,而 JDI 只是實現了部分 JVMTI 的方法,所以某些專業的 Profiler 工具,可能會直接使用 JVMTI,從而實現更豐富的診斷分析功能。
遠端除錯與本地除錯
不知道大家有沒有留意過本地 Debug 啟動時的日誌:
第一行是隱藏了後半段的啟動命令,展開後是這個樣子:
/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 ...
第二行是一個 Connected 日誌,意思是使用 socket 連線到遠端 VM 的53631埠
上一段說到,IDE 透過 JDI 介面,使用 JDWP 協議和目標 VM 的 JVMTI 互動。這裡的 53631 埠,就是目標 JVM 暴露出的 JVM TI 的 server 埠。
而第一行裡,IDEA 自動給我們加上了 -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53631
這麼一段,這個引數的意思就是,讓 jvm 以 53631 暴露 jdwp 協議
小知識,這個 agentlib 可不只是為 jvmti 提供的。它還可以讓 JVM 載入其他的 native lib包,直接“外掛”到你的 jvm 上,下面是“外掛”的引數格式:
所以吧,上面的描述其實不太嚴謹,更專業的說法是:
讓 JVM 載入 JDWP 這個 agent 庫,引數為transport=dt_socket,address=127.0.0.1:53631
,這個 jdwp agent 庫以 53631 埠提供了 jdwp 協議的 server。只不過這個 jdwp 是jvm 內部的庫,不需要額外的 so/dylib/dll 檔案。
如有需要,你完全可以弄個 “datupiao” 的 agentlib,“外掛”到這個 jvm 上,然後在這個 lib 裡呼叫 JVMTI 介面,然後暴露個埠提供服務和遠端互動,實現自己的 jdwp!
可能某些老闆們注意到了,本地除錯還要127.0.0.1走tcp 互動一遍,那遠端除錯呢?
基於上面的解釋,本地除錯和遠端除錯真的沒啥區別!或者說,在目前 IDEA/Eclipse 的實現下,不存在本地除錯,都是遠端!只不過一個是 127.0.0.1,一個是遠端的 IP 而已。
在本地除錯時,IDEA 會自動給我們的 JVM 增加 agent
引數,隨機指定一個埠,然後透過 JDI 介面連線,程式碼大概長這樣(JDI 的 SDK 在 JDK_HOME/lib/tools.jar ):
Map<String, Connector.Argument> env = connector.defaultArguments();
env.get("hostname").setValue(hostname);
env.get("port").setValue(port);
VirtualMachine vm = connector.attach(env);
瞅瞅, VirtualMachine 裡的就這點方法,能力上比 JVMTI 還是差遠了
List<ReferenceType> classesByName(String className);
List<ReferenceType> allClasses();
void redefineClasses(Map<? extends ReferenceType, byte[]> classToBytes);
List<ThreadReference> allThreads();
void suspend();
void resume();
List<ThreadGroupReference> topLevelThreadGroups();
EventQueue eventQueue();
EventRequestManager eventRequestManager();
VoidValue mirrorOfVoid();
Process process();
再回來看看 IDEA 中獨立的遠端除錯,配置好之後,紅框裡的資訊會提示你 ,遠端的 JVM 需增加這一段啟動引數,而且支援多個版本 JDK 的格式,CV 大法就能直接用。
-agentlib 和 -javaagent
有些細心的同學可能發現了,IDEA 預設的啟動指令碼里,同時配置了 -agentlib 和 -javaagent。
-javaagent:/path/to/jetbrains/debugger-agent.jar
這個 debugger-agent吧,其實也沒幹啥事,只是對 JDK 內建的一些執行緒做了些增強,輔助 IDEA 的 debug 功能,支援一些非同步的除錯。
agentlib、javaagent 這倆兄弟,定位其實很像,都是載入自定義的程式碼。
不過區別在於,agentlib 是載入 native lib,需要c/cpp 去寫,相當於外掛自己的程式碼在 jvm 上,可以為所欲為,比如在 agentlib 裡呼叫上面說的 JVMTI 。
而 javaagent 是用 java 寫的,可以直接用上層的 Instrumentation API,做一些類的增強轉換之類,這也是大多數 APM Agent、Profiler Agent實現的基本原理。
Arthas 的玩法
Arthas 的核心入口,其實還是 javaagent,支援靜態載入和動態載入兩種玩法。
靜態沒啥好說的,啟動指令碼里增加一個-javaagent:/tmp/test/arthas-agent.jar
,然後為所欲為。
動態的叫 attach,使用 Java 提供的 VirtualMachine
就可以實現執行時新增 -javaagent,效果一樣:
VirtualMachine virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);
virtualMachine.loadAgent(agentPath, agentArgs);
這個 Agent 在 JVM 裡啟動了一個TCP server,用於收發 Arthas Client 的各種 trace、watch 、Dashboard 等指令,然後透過 Instrumentation 增強Class 插入程式碼、或者直接呼叫某些 Java API,實現各種功能。
注意到了嗎?Arthas 可以直接下載一個 jar 包,java -jar 就能連上。
其實吧,它這個直接啟動的 jar 包,是一個 boot 包,啟動之後把亂七八糟的 jar 都下載下來。接著動態 attach 的方式,連線到本機指定程序號的 JVM,然後再為所欲為。
在 3.5 版本之後,Arthas 還新增了一個 vmtool 命令,這個命令可以直接獲取記憶體中的指定物件例項。
$ 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],
]
直接獲取記憶體物件,這玩意只靠 Instrumentation API 可做不到。Arthas 搞了個騷操作,直接 JNI 呼叫自定義 lib,用過 cpp 直接呼叫了 JVMTI 的 API,融合了 Instrumentation 和 JVMTI 的能力,這下是真的為所欲為了!
#include <stdio.h>
#include <jni.h>
#include <jni_md.h>
#include <jvmti.h>
#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!%u\n", error);
return NULL;
}
jint count = 0;
jobject *instances;
error = jvmti->GetObjectsWithTags(1, &tag, &count, &instances, NULL);
if (error) {
printf("ERROR: JVMTI GetObjectsWithTags failed!%u\n", error);
return NULL;
}
jobjectArray array = env->NewObjectArray(count, klass, NULL);
//新增元素到陣列
for (int i = 0; i < count; i++) {
env->SetObjectArrayElement(array, i, instances[i]);
}
jvmti->Deallocate(reinterpret_cast<unsigned char *>(instances));
return array;
}
總結
-
Debug 基於 JDPA 體系
-
IDE 直接接入 JDPA 體系中的 JDI 介面完成
-
JDI 透過 JDWP 協議,呼叫遠端 VM 的 JVMTI 介面
-
JDWP 是透過 agentlib 載入的,agentlib 算是一個 native 的靜態“外掛”介面
-
-
javaagent 是 JAVA 層面的“外掛”介面,用過 Instrumentation API(Java)實現各種功能,主要用於APM、Profiler 工具
-
如果你想,在 javaagent 裡呼叫功能更豐富的 JVMTI 也不是不行。