使用JVMTI獲取Java多線程程序指令執行次序

使用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


擴展閱讀:

1. JVMTM Tool Interface 文檔

2. JVMTI官方教程

3. JVMTI官方Demo:下載後可在目錄 jdk1.8.0_05/demo/jvmti/ 下找到多個Demo

發佈了91 篇原創文章 · 獲贊 235 · 訪問量 71萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章