管理堆空間:使用JVMTI循環類實例

y37f 9年前發布 | 16K 次閱讀 JVMTI Java開發

今天我想探討Java的另一面,我們平時不會注意到或者不會使用到的一面。更準確的說是關于底層綁定、本地代碼(native code)以及如何實現一些小魔法。雖然我們不會在JVM層面上探究這是怎么實現的,但我們會通過這篇文章展示一些奇跡

我在ZeroTurnaround的RebelLabs團隊中主要工作是做研究、撰文、編程。這個公司主要開發面向Java開發者的工具,大部分以Java插件(javaagent)的方式運行。經常會遇到這種情況,如果你想在不重寫JVM的前提下增強JVM或者提高它的性能,你就必須深入研究Java插件的神奇世界。插件包括兩類:Java javaagents和Native javaagents。本文主要討論后者。

Anton Arhipov——XRebel產品的領導者–在布拉格的GeeCON會議上做了“Having fun with Javassist”的演講。這個演講可以作為了解完全使用Java開發javaagents的一個起點。

本文中,我們會創建一個小的Native JVM插件,探究向Java應用提供Native方法的可能性以及如何使用Java虛擬機工具接口(JVM TI)

如果你想從本文獲取一些干貨,那是必須的。劇透下,我們可以計算給定類在堆空間中包含多少實例。

假設你是圣誕老人值得信賴的一個黑客精靈,圣誕老人有一些挑戰讓你做:

Santa: 我親愛的黑客精靈,你能寫一個程序,算出當前JVM堆中有多少Thread實例嗎?

一個不喜歡挑戰自己的精靈可能會答道: 很簡單,不是么?

return Thread.getAllStackTraces().size();

但是如果把問題改為任意給定類(不限于Thread),如何重新設計我們的方案呢?我們是不是得實現下面這個接口?

public interface HeapInsight {
  int countInstances(Class klass);
}

這不可能吧?如果String.class作為輸入參數會怎么樣呢? 不要害怕,我們只需深入到JVM內部一點。對JVM庫開發者來說,可以使用JVMTI,一個Java虛擬機工具接口(Java Virtual Machine Tool Interface)。JVMTI添加到Java中已經很多年了,很多有意思的工具都使用JVMTI。JVMTI提供了兩類接口:

  • Native API
  • Instrumentation API,用來監控并轉換加載到JVM中類的字節碼

在我們的例子中,我們要使用Native API。我們想要用的是IterateThroughHeap函數,我們可以提供一個自定義的回調函數,對給定類的每個實例都可以執行回調函數。

首先,我們先創建一個Native插件,可以加載并顯示一些東西,以確保我們的架構沒問題。

Native插件是用C/C++實現的,并編譯為一個動態庫,它在我們開始考慮Java前就已經被加載了。如果你對C++不熟,沒關系,很多精靈都不熟,而且也不難。我寫C++時主要有兩個策略:靠巧合編程、避免段錯誤。所以,當我準備寫下本文的代碼和說明時,我們都可以練一遍。

下面就是創建的第一個native插件:

#include 
#include 

using namespace std;

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
{
  cout << "A message from my SuperAgent!" << endl;
  return JNI_OK;
}

最重要的部分就是我們根據動態鏈接插件的文檔聲明了一個Agent_OnLoad的函數,

保存文件為“native-agent.cpp”,接下來讓我們把它編譯為動態庫。

我用的是OSX,所以我可以使用clang編譯。為了節省你google搜索的功夫,下面是完整的命令:

clang -shared -undefined dynamic_lookup -o agent.so -I /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include/ -I /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/include/darwin native-agent.cpp

這會生成一個agent.so文件,就是供我們使用的動態庫。為了測試它,我們創建一個hello world類。

package org.shelajev;
public class Main {
   public static void main(String[] args) {
       System.out.println("Hello World!");
   }
}

當你運行時,使用-agentpath選項正確地指向agent.so文件,你應該可以看到以下輸出:

java -agentpath:agent.so org.shelajev.Main
A message from my SuperAgent!
Hello World!

做的不錯!現在,我們準備讓這個插件真正地起作用。首先,我們需要一個jvmtiEnv實例。它可以在Agent_OnLoad執行時通過`JavaVM jvm`獲得,但之后就不行了。所以我們必須把它保存在一個可全局訪問的地方。我們聲明了一個全局結構體來保存它。

#include 
#include 

using namespace std;

typedef struct {
 jvmtiEnv *jvmti;
} GlobalAgentData;

static GlobalAgentData *gdata;

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
{
  jvmtiEnv *jvmti = NULL;
  jvmtiCapabilities capa;
  jvmtiError error;

  // put a jvmtiEnv instance at jvmti.
  jint result = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_1);
  if (result != JNI_OK) {
    printf("ERROR: Unable to access JVMTI!\n");
  }
  // add a capability to tag objects
  (void)memset(∩a, 0, sizeof(jvmtiCapabilities));
  capa.can_tag_objects = 1;
  error = (jvmti)->AddCapabilities(∩a);

  // store jvmti in a global data
  gdata = (GlobalAgentData*) malloc(sizeof(GlobalAgentData));
  gdata->jvmti = jvmti;
  return JNI_OK;
}

我們也更新了部分代碼,讓jvmti實例可以使用對象tag(tag:對象附帶一個值,參見JVMTI文檔),因為遍歷堆的時候需要這么做。準備都已就緒,我們擁有了已初始化的JVMTI實例。我們通過JNI將它提供給Java代碼使用。

JNI表示Java Native Interface,是在Java應用中調用native代碼的標準方式。Java部分相當簡單直接,在Main類中添加countInstances方法的定義,如下所示:

package org.shelajev;

public class Main {
   public static void main(String[] args) {
       System.out.println("Hello World!");
       int a = countInstances(Thread.class);
       System.out.println("There are " + a + " instances of " + Thread.class);
   }

   private static native int countInstances(Class klass);
}

為了適應native方法,我們必須修改我們的native插件代碼。我稍后會解釋,現在在其中添加下面的函數定義:

extern "C"
JNICALL jint objectCountingCallback(jlong class_tag, jlong size, jlong* tag_ptr, jint length, void* user_data) 
{
 int* count = (int*) user_data;
 *count += 1; 
 return JVMTI_VISIT_OBJECTS;
}

extern "C"
JNIEXPORT jint JNICALL Java_org_shelajev_Main_countInstances(JNIEnv *env, jclass thisClass, jclass klass) 
{
 int count = 0;
   jvmtiHeapCallbacks callbacks;
(void)memset(&callbacks, 0, sizeof(callbacks));
callbacks.heap_iteration_callback = &objectCountingCallback;
 jvmtiError error = gdata->jvmti->IterateThroughHeap(0, klass, &callbacks, &count);
 return count;
}

這里的Java_org_shelajev_Main_countInstances方法更有趣,它以“Java”開始,接著以“_”分隔的完整類名稱,最后是Java中的方法名。同樣不要忘記了JNIEXPORT聲明,表示這個方法將要導入到Java世界中。

在Java_org_shelajev_Main_countInstances函數內部,首先我們聲明了objectCountingCallback函數作為回調函數,然后調用IterateThroughHeap函數,它的參數通過Java程序傳入。

注意,我們的native方法是靜態的,所以C語言對應的參數是:

JNIEnv *env, jclass thisClass, jclass klass

for an instance method they would be a bit different: 如果是實例方法的話,參數會有點不一樣:

JNIEnv *env, jobj thisInstance, jclass klass

其中thisInstance指向調用Java方法的實例。

現在直接根據文檔給出objectCountingCallback的定義,主要內容不過是遞增一個int變量。

搞定了!感謝你的耐心。如果你仍在閱讀,你可以嘗試運行上述的代碼。

重新編譯native插件,并運行Main class。我的結果如下:

java -agentpath:agent.so org.shelajev.Main
Hello World!
There are 7 instances of class java.lang.Thread

如果我在main方法中添加一行Thread t = new Thread();,結果就是8個。看上去插件確實起作用了。你的數目肯定會和我不一樣,沒事,這很正常,因為它要算上統計、編譯、GC等線程。

如果我想知道堆內存中String的數量,只需改變class參數。這是一個真正泛型的解決方案,我想圣誕老人會高興的。

你對結果感興趣的話,我告訴你,結果是2423個String實例。對這么個小程序來說,數量相當大了。

如果執行:

return Thread.getAllStackTraces().size();

結果是5,不是8。因為它沒有算上統計線程。還要考慮這種簡單的解決方案么?

現在,通過本文和相關知識的學習,我不敢說你可以開始寫自己的JVM監控或增強工具,但這肯定是一個起點。

在本文中,我們從零開始寫了一個Java native插件,編譯、加載、并成功運行。這個插件使用JVMTI來深入JVM內部(否則無法做到)。對應的Java代碼調用native庫并生成結果。

這是很多優秀的JVM工具經常采用的策略,我希望我已經為你解釋清楚了其中的一些技巧。

原文鏈接: JavaCodeGeeks 翻譯: ImportNew.com - 文 學敏
譯文鏈接: http://www.importnew.com/15298.html

 本文由用戶 y37f 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!