使用JVMTI獲取Java多線程程序指令執行次序
在Java多線程程序中,由於線程調度,指令間的次序在每次運行時都可能不相同,有時候,我們需要得到指令次序,用來分析程序的行爲。這樣細粒度的底層行爲用一般方法很難完成,我們需要藉助 JVM Tool Interface,即JVMTI,來幫助我們獲取Java虛擬機執行時的信息。本文先介紹編寫JVMTI程序的基本框架,然後介紹如何使用JVMTI來獲取多線程程序中指令之間的次序。
JVMTI簡介
JVMTI是用於編寫開發與監視工具的編程接口,使用它可以檢查並控制運行於Java虛擬機上的程序。使用它可以完成性能分析,調試,監視(monitoring),線程分析,覆蓋分析(coverage analysis)等工具。
使用JVMTI可以編寫出一個agent。在運行Java程序時,指定這個agent,那麼當虛擬機運行程序時,如果agent中指定的一些事件發生,虛擬機就會調用agent中相應的回調函數。JVMTI提供了一系列可以指定的事件,以及獲取虛擬機中信息的函數接口。
JVMTI基本編程方法
編寫agent
- 頭文件 agent程序中,需要包含
jvmti.h
頭文件,才能使用JVMTI中提供的接口。
#include <jvmti.h>
-
基本事件 和agent有關的兩個基本事件是agent的啓動與關閉,我們需要自己編寫與啓動與關閉相關的函數,這樣,虛擬機才知道啓動與關閉agent時,都需要做些什麼。
與啓動相關的函數有兩個,如果你的agent在虛擬機處於
OnLoad
階段時啓動,會調用Agent_OnLoad
函數,如果你的agent在虛擬機處於Live
階段時啓動,會調用Agent_OnAttach
函數。我的理解是,如果你的agent想要全程監視一個程序的運行,就編寫
Agent_OnLoad
,並在啓動虛擬機時指定agent。如果你的agent想獲取一個已經在運行的虛擬機中程序的信息,就編寫Agent_OnAttach
。兩個函數的原型如下:
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char *options, void *reserved)
與agent關閉相關的函數是Agent_OnUnload
,當agent要被關閉時,虛擬機會調用這個函數,函數原型爲:
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm)
- 程序基本框架
主要的內容框架在Agent_OnLoad
中編寫:
1 獲取jvm環境
/* get env */
jvmtiEnv *jvmti = NULL;
jvmtiError error;
error = (*jvm)->GetEnv(jvm, (void **)&jvmti, JVMTI_VERSION);
if (error != JNI_OK) {
fprintf(stderr, "Couldn't get JVMTI environment");
return JNI_ERR;
}
可以爲同一個虛擬機指定多個agent,每個agent都有自己的環境,在指定agent行爲前,首先要獲取的就是環境信息,後面的操作都是針對這個環境的。另外,JVMTI中的函數都會返回錯誤代碼,在調用函數後,需要檢查返回值,以確定函數調用是否成功。不同的函數會返回不同類型的錯誤碼,可自行參閱JVMTI的API。
另外,需要注意,JVMTI程序可以使用C/C++編寫,兩者在調用函數時略有不同,上面的例子是用C編寫,gcc編譯。如果你使用C++編寫,GetEnv
需要這樣調用:
error = (jvm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_1);
其它函數依次類推。
2 添加capabilities
JVMTI中有很多事件,每個事件都對對應一些Capabilities,如果你想爲此事件編寫函數,就要開啓相應的Capabilities,例如,我們想對 JVMTI_EVENT_SINGLE_STEP
事件編寫函數,可以查到,需要開啓can_generate_single_step_events
:
/* add capabilities */]
jvmtiCapabilities capa;
memset(&capa, 0, sizeof(jvmtiCapabilities));
capa.can_generate_single_step_events = 1;
error = (*jvmti)->AddCapabilities(jvmti, &capa);
check_jvmti_error(jvmti, error, \
"Unable to get necessary JVMTI capabilities.");
如果開啓的Capabilities多於一個,不用聲明多個jvmtiCapabilities
變量,只需要使用類似
capa.can_generate_single_step_events = 1;
的方式指定就行。
3 指定事件
JVMTI編寫的目的是,當虛擬機中一個事件發生時,調用我們爲此事件編寫的函數。所以我們需要指定哪個事件發生時,通知agent:
/* set events */
error = (*jvmti)->SetEventNotificationMode \
(jvmti, JVMTI_ENABLE, JVMTI_EVENT_SINGLE_STEP, NULL);
check_jvmti_error(jvmti, error, "Cannot set event notification");
其中 JVMTI_EVENT_SINGLE_STEP
就是事件代碼。
需要特別注意的是,要先開啓相關capabilities,然後才能指定事件。
4 設置回調函數
我們還需要爲事件指定回調函數,並自行編寫回調函數,事件回調函數的接口是由JVMTI指定的,例如JVMTI_EVENT_SINGLE_STEP
事件的回調函數原型:
void JNICALL
SingleStep(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jmethodID method,
jlocation location)
爲事件指定回調函數的方法是:
jvmtiEventCallbacks callbacks;
/* add callbacks */
memset(&callbacks, 0, sizeof(callbacks));
callbacks.SingleStep = &callbackSingleStep;
error = (*jvmti)->SetEventCallbacks \
(jvmti, &callbacks, (jint)sizeof(callbacks));
check_jvmti_error(jvmti, error, "Canot set jvmti callbacks");
之後,我們需要自己編寫 callbackSingleStep
函數:
void JNICALL
callbackSingleStep(
jvmtiEnv *jvmti,
JNIEnv* jni,
jthread thread,
jmethodID method,
jlocation location) {
}
運行agent
運行agent,通過指定虛擬機參數來設定,例如運行PossibleReordering
時:
java -classpath . \
-agentpath:`pwd`/jvmagent/TraceAgent.so PossibleReordering
其中TraceAgent.so
就是編譯後生成的agent。
使用JVMTI獲取多線程程序指令執行次序
我們知道,在Java虛擬機中的運行時數據區中,每個線程都有它的私有區域,每個線程有自己的PC寄存器,PC寄存器表示線程當前執行的指令在內存中的地址。其實我最初的目的是想得到這個PC的值,但是找了很久都沒有找到,然後在JVMTI中找到了類似的概念。
在JVMTI中,介紹單步事件(Single Step Event)時說,當一個線程到達一個新的位置(location)時,單步事件就會產生。單步事件使agent以虛擬機允許的最細粒度,跟蹤線程執行。
我們回到單步事件回調函數的原型:
void JNICALL
SingleStep(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jmethodID method,
jlocation location)
其中的 location
就是新指令的位置。
我們首先來編寫一個Java多線程程序,這個程序是 《Java併發編程實戰》(Java Concurrency in Practice) 中的一個例子,我做了一點變形:
import java.lang.Thread;
public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
a = 1;
x = b;
a = 1;
x = b;
a = 1;
x = b;
a = 1;
x = b;
a = 1;
x = b;
a = 1;
x = b;
a = 1;
x = b;
}
});
one.start();
other.start();
one.join();
other.join();
}
}
給Thread other多加了一些語句,用以區分兩個線程。
這裏有一個問題是,我們關心的其實只是兩個線程的 run
函數中指令的次序,而單步事件會在任何指令執行時,都調用回調函數,這就需要我們在回調函數中,只保留源代碼中的兩個線程的run
函數中的指令的位置,其它的都過濾掉。
我們可以使用JVMTI提供的 GetMethodName
來得到函數名,使用 GetMethodDeclaringClass
得到類名,然後通過比較類名和函數名,只保留 run
中的指令:
error = (*jvmti)->GetMethodName( \
jvmti, method, &method_name, &method_signature, SKIP_GENERIC);
error = (*jvmti)->GetMethodDeclaringClass( \
jvmti, method, &declaring_class);
if (strncmp(method_name, "run", 4) == 0 && \
strstr(class_signature, "PossibleReordering") != NULL) {
printf("%s\t", thread_info.name);
printf("%s\t", class_signature);
printf("%lld\t", location);
printf("%s %lld:%lld\t", method_name, s_location, e_location);
printf("\n");
}
執行下列命令:
java -classpath . -agentpath:`pwd`/jvmagent/TraceAgent.so=log.txt PossibleReordering
即可得到指令次序信息:
Thread-0 LPossibleReordering$1; 0 run 0:10
Thread-0 LPossibleReordering$1; 1 run 0:10
Thread-1 LPossibleReordering$2; 0 run 0:80
Thread-0 LPossibleReordering$1; 4 run 0:10
Thread-0 LPossibleReordering$1; 7 run 0:10
Thread-0 LPossibleReordering$1; 10 run 0:10
Thread-1 LPossibleReordering$2; 1 run 0:80
Thread-1 LPossibleReordering$2; 4 run 0:80
Thread-1 LPossibleReordering$2; 7 run 0:80
Thread-1 LPossibleReordering$2; 10 run 0:80
Thread-1 LPossibleReordering$2; 11 run 0:80
Thread-1 LPossibleReordering$2; 14 run 0:80
Thread-1 LPossibleReordering$2; 17 run 0:80
Thread-1 LPossibleReordering$2; 20 run 0:80
Thread-1 LPossibleReordering$2; 21 run 0:80
Thread-1 LPossibleReordering$2; 24 run 0:80
Thread-1 LPossibleReordering$2; 27 run 0:80
Thread-1 LPossibleReordering$2; 30 run 0:80
Thread-1 LPossibleReordering$2; 31 run 0:80
Thread-1 LPossibleReordering$2; 34 run 0:80
Thread-1 LPossibleReordering$2; 37 run 0:80
Thread-1 LPossibleReordering$2; 40 run 0:80
Thread-1 LPossibleReordering$2; 41 run 0:80
Thread-1 LPossibleReordering$2; 44 run 0:80
Thread-1 LPossibleReordering$2; 47 run 0:80
Thread-1 LPossibleReordering$2; 50 run 0:80
Thread-1 LPossibleReordering$2; 51 run 0:80
Thread-1 LPossibleReordering$2; 54 run 0:80
Thread-1 LPossibleReordering$2; 57 run 0:80
Thread-1 LPossibleReordering$2; 60 run 0:80
Thread-1 LPossibleReordering$2; 61 run 0:80
Thread-1 LPossibleReordering$2; 64 run 0:80
Thread-1 LPossibleReordering$2; 67 run 0:80
Thread-1 LPossibleReordering$2; 70 run 0:80
Thread-1 LPossibleReordering$2; 71 run 0:80
Thread-1 LPossibleReordering$2; 74 run 0:80
Thread-1 LPossibleReordering$2; 77 run 0:80
Thread-1 LPossibleReordering$2; 80 run 0:80
最終的源代碼中,我還輸出了線程名,和方法的指令地址範圍。
我們可以反編譯 PossibleReordering$1
和 PossibleReordering$2
,看看相應的指令範圍是否可以對應上。
$ javap -c PossibleReordering\$1.class
final class PossibleReordering$1 implements java.lang.Runnable {
PossibleReordering$1();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void run();
Code:
0: iconst_1
1: putstatic #2 // Field PossibleReordering.a:I
4: getstatic #3 // Field PossibleReordering.b:I
7: putstatic #4 // Field PossibleReordering.x:I
10: return
}
$ javap -c PossibleReordering\$2.class
Compiled from "PossibleReordering.java"
final class PossibleReordering$2 implements java.lang.Runnable {
PossibleReordering$2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void run();
Code:
0: iconst_1
1: putstatic #2 // Field PossibleReordering.b:I
4: getstatic #3 // Field PossibleReordering.a:I
7: putstatic #4 // Field PossibleReordering.y:I
10: iconst_1
11: putstatic #3 // Field PossibleReordering.a:I
14: getstatic #2 // Field PossibleReordering.b:I
17: putstatic #5 // Field PossibleReordering.x:I
20: iconst_1
21: putstatic #3 // Field PossibleReordering.a:I
24: getstatic #2 // Field PossibleReordering.b:I
27: putstatic #5 // Field PossibleReordering.x:I
30: iconst_1
31: putstatic #3 // Field PossibleReordering.a:I
34: getstatic #2 // Field PossibleReordering.b:I
37: putstatic #5 // Field PossibleReordering.x:I
40: iconst_1
41: putstatic #3 // Field PossibleReordering.a:I
44: getstatic #2 // Field PossibleReordering.b:I
47: putstatic #5 // Field PossibleReordering.x:I
50: iconst_1
51: putstatic #3 // Field PossibleReordering.a:I
54: getstatic #2 // Field PossibleReordering.b:I
57: putstatic #5 // Field PossibleReordering.x:I
60: iconst_1
61: putstatic #3 // Field PossibleReordering.a:I
64: getstatic #2 // Field PossibleReordering.b:I
67: putstatic #5 // Field PossibleReordering.x:I
70: iconst_1
71: putstatic #3 // Field PossibleReordering.a:I
74: getstatic #2 // Field PossibleReordering.b:I
77: putstatic #5 // Field PossibleReordering.x:I
80: return
}
可以看出,確實是一個線程的run方法指令範圍是 0:10
,另一個是 0:80
,說明我們正確獲取了相應指令。
完整的源代碼,包含如何編譯,運行,可以在我的GitHub中找到:AgentDemo
擴展閱讀:
2. JVMTI官方教程
3. JVMTI官方Demo:下載後可在目錄 jdk1.8.0_05/demo/jvmti/ 下找到多個Demo