管理堆空間:使用JVMTI循環類實例
今天我想探討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