天天看點

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

在java多線程程式中,由于線程排程,指令間的次序在每次運作時都可能不相同,有時候,我們需要得到指令次序,用來分析程式的行為。這樣細粒度的底層行為用一般方法很難完成,我們需要借助 ,即jvmti,來幫助我們擷取java虛拟機執行時的資訊。本文先介紹編寫jvmti程式的基本架構,然後介紹如何使用jvmti來擷取多線程程式中指令之間的次序。

jvmti是用于編寫開發與監視工具的程式設計接口,使用它可以檢查并控制運作于java虛拟機上的程式。使用它可以完成性能分析,調試,監視(monitoring),線程分析,覆寫分析(coverage analysis)等工具。

使用jvmti可以編寫出一個agent。在運作java程式時,指定這個agent,那麼當虛拟機運作程式時,如果agent中指定的一些事件發生,虛拟機就會調用agent中相應的回調函數。jvmti提供了一系列可以指定的事件,以及擷取虛拟機中資訊的函數接口。

頭檔案 agent程式中,需要包含 <code>jvmti.h</code>頭檔案,才能使用jvmti中提供的接口。

基本事件 和agent有關的兩個基本事件是agent的啟動與關閉,我們需要自己編寫與啟動與關閉相關的函數,這樣,虛拟機才知道啟動與關閉agent時,都需要做些什麼。

與啟動相關的函數有兩個,如果你的agent在虛拟機處于<code>onload</code>階段時啟動,會調用<code>agent_onload</code>函數,如果你的agent在虛拟機處于<code>live</code>階段時啟動,會調用<code>agent_onattach</code>函數。

我的了解是,如果你的agent想要全程監視一個程式的運作,就編寫<code>agent_onload</code>,并在啟動虛拟機時指定agent。如果你的agent想擷取一個已經在運作的虛拟機中程式的資訊,就編寫<code>agent_onattach</code>。

兩個函數的原型如下:

與agent關閉相關的函數是<code>agent_onunload</code>,當agent要被關閉時,虛拟機會調用這個函數,函數原型為:

程式基本架構

主要的内容架構在<code>agent_onload</code>中編寫:

1 擷取jvm環境

可以為同一個虛拟機指定多個agent,每個agent都有自己的環境,在指定agent行為前,首先要擷取的就是環境資訊,後面的操作都是針對這個環境的。另外,jvmti中的函數都會傳回錯誤代碼,在調用函數後,需要檢查傳回值,以确定函數調用是否成功。不同的函數會傳回不同類型的錯誤碼,可自行參閱jvmti的api。

另外,需要注意,jvmti程式可以使用c/c++編寫,兩者在調用函數時略有不同,上面的例子是用c編寫,gcc編譯。如果你使用c++編寫,<code>getenv</code>需要這樣調用:

其它函數依次類推。

2 添加capabilities

jvmti中有很多事件,每個事件都對對應一些capabilities,如果你想為此事件編寫函數,就要開啟相應的capabilities,例如,我們想對 <code>jvmti_event_single_step</code> 事件編寫函數,可以查到,需要開啟<code>can_generate_single_step_events</code>:

如果開啟的capabilities多于一個,不用聲明多個<code>jvmticapabilities</code>變量,隻需要使用類似

的方式指定就行。

3 指定事件

jvmti編寫的目的是,當虛拟機中一個事件發生時,調用我們為此事件編寫的函數。是以我們需要指定哪個事件發生時,通知agent:

其中 <code>jvmti_event_single_step</code> 就是事件代碼。

需要特别注意的是,要先開啟相關capabilities,然後才能指定事件。

4 設定回調函數

我們還需要為事件指定回調函數,并自行編寫回調函數,事件回調函數的接口是由jvmti指定的,例如<code>jvmti_event_single_step</code>事件的回調函數原型:

為事件指定回調函數的方法是:

之後,我們需要自己編寫 <code>callbacksinglestep</code>函數:

運作agent,通過指定虛拟機參數來設定,例如運作<code>possiblereordering</code>時:

其中<code>traceagent.so</code>就是編譯後生成的agent。

我們知道,在java虛拟機中的運作時資料區中,每個線程都有它的私有區域,每個線程有自己的pc寄存器,pc寄存器表示線程目前執行的指令在記憶體中的位址。其實我最初的目的是想得到這個pc的值,但是找了很久都沒有找到,然後在jvmti中找到了類似的概念。

在jvmti中,介紹單步事件(single step event)時說,當一個線程到達一個新的位置(location)時,單步事件就會産生。單步事件使agent以虛拟機允許的最細粒度,跟蹤線程執行。

我們回到單步事件回調函數的原型:

其中的 <code>location</code> 就是新指令的位置。

我們首先來編寫一個java多線程程式,這個程式是 《java并發程式設計實戰》(java concurrency in practice) 中的一個例子,我做了一點變形:

給thread other多加了一些語句,用以區分兩個線程。

這裡有一個問題是,我們關心的其實隻是兩個線程的 <code>run</code>函數中指令的次序,而單步事件會在任何指令執行時,都調用回調函數,這就需要我們在回調函數中,隻保留源代碼中的兩個線程的<code>run</code>函數中的指令的位置,其它的都過濾掉。

我們可以使用jvmti提供的 <code>getmethodname</code> 來得到函數名,使用 <code>getmethoddeclaringclass</code>得到類名,然後通過比較類名和函數名,隻保留 <code>run</code>中的指令:

執行下列指令:

即可得到指令次序資訊:

最終的源代碼中,我還輸出了線程名,和方法的指令位址範圍。

我們可以反編譯 <code>possiblereordering$1</code> 和 <code>possiblereordering$2</code>,看看相應的指令範圍是否可以對應上。

可以看出,确實是一個線程的run方法指令範圍是 <code>0:10</code> ,另一個是 <code>0:80</code>,說明我們正确擷取了相應指令。

完整的源代碼,包含如何編譯,運作,可以在我的github中找到:

擴充閱讀:

1. 

2.

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