拥有您的堆:使用JVMTI迭代类实例
今天,我想談一談我們大多數人每天都不會看到和使用的另一種Java,更確切地說,是有關較低級別的綁定,一些本機代碼以及如何執行一些小的魔術。 盡管我們不會在JVM上找到真正的魔力源,但是在單個帖子的范圍內可以實現一些小奇跡。
我花了整整一天的時間在ZeroTurnaround的RebelLabs團隊進行研究,編寫和編碼,該公司為Java開發人員創建工具,這些工具主要以javaagents的身份運行。 通常情況下,如果您想在不重寫JVM的情況下增強JVM或在JVM上獲得任何不錯的功能,則必須深入研究Java代理的美麗世界。 這些有兩種風格:Java javaagents和本機Javaagents。 在這篇文章中,我們將集中討論后者。
注意, XRebel產品負責人Anton Arhipov的這個GeeCON Prague演示文稿是學習完全用Java編寫的javaagents的一個很好的起點: 與Javassist一起玩 。
在本文中,我們將創建一個小型的本機JVM代理,探討將本機方法公開到Java應用程序中的可能性,并了解如何利用Java虛擬機工具接口 。
如果您想從這篇文章中找到實用的方法,我們將能夠在擾亂警報的情況下計算堆中存在給定類的實例數量。
想象一下,您是圣誕老人值得信賴的黑客精靈,而這位大人物對您來說面臨以下挑戰:
圣誕老人: 我親愛的Hacker Elf,您能否編寫一個程序來指出JVM堆中當前隱藏了多少個Thread對象?
另一個不愿意挑戰自己的小精靈會回答: 這很容易直接,對嗎?
return Thread.getAllStackTraces().size();但是,如果我們要過度設計解決方案以能夠回答有關任何給定類的問題,該怎么辦? 說我們要實現以下接口?
public interface HeapInsight {int countInstances(Class klass); }是的,那是不可能的,對吧? 如果您將String.class作為參數接收怎么辦? 不用擔心,我們只需要更深入地研究JVM的內部結構。 JVMTI作者可以使用的一件事是JVMTI (Java虛擬機工具接口)。 它是很久以前添加的,許多看似神奇的工具都在使用它。 JVMTI提供兩件事:
- 本機API
- 一種工具API,用于監視和轉換裝入JVM的類的字節碼。
就我們的示例而言,我們需要訪問本機API。 我們要使用的是IterateThroughHeap函數,該函數使我們可以提供一個自定義回調,以對給定類的每個對象執行該回調。
首先,讓我們創建一個本地代理,該代理將加載和回顯某些內容,以確保我們的基礎架構能夠正常工作。
本機代理是用C / C ++編寫的東西,并編譯成一個動態庫,在我們甚至開始考慮Java之前就已經加載了。 如果您不精通C ++,請不要擔心,沒有很多精靈,也不會很難。 我使用C ++的方法包括2種主要策略:巧合編程和避免段錯誤。 因此,由于我設法編寫了這篇文章的示例代碼并對其進行了注釋,因此,我們可以一起研究一下。 注意:以上段落應作為免責聲明,請勿將此代碼置于任何對您有價值的環境中。
這是創建第一個本機代理的方法:
#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對其進行編譯,以節省一些時間,下面是完整的命令:
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 Java類。
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 datagdata = (GlobalAgentData*) malloc(sizeof(GlobalAgentData));gdata->jvmti = jvmti;return JNI_OK; }我們還更新了代碼,以添加標記對象的功能,這是我們遍歷堆所需的。 現在準備工作已經完成,我們已經初始化了JVMTI實例并且可供我們使用。 讓我們通過JNI將其提供給我們的Java代碼。
JNI代表Java本機接口 ,這是將本機代碼調用包含到Java應用程序中的一種標準方式。 Java部分將非常簡單,將以下countInstances方法定義添加到Main類:
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); }為了適應本機方法,我們必須更改本機代理代碼。 我將在稍后解釋,但現在在其中添加以下函數定義:
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函數指定為回調,并使用Java應用程序中的參數調用IterateThroughHeap 。
請注意,我們的本機方法是靜態的,因此C副本中的參數為:
JNIEnv *env, jclass thisClass, jclass klass對于實例方法,它們將有所不同:
JNIEnv *env, jobj thisInstance, jclass klass這里的thisInstance指向Java方法調用的this對象。
現在, objectCountingCallback的定義直接來自文檔 。 身體無非就是增加一個int。
繁榮! 全做完了! 感謝您的耐心等待。 如果您仍在閱讀本文,則可以測試上面的所有代碼。
再次編譯本機代理并運行Main類。 這是我看到的:
java -agentpath:agent.so org.shelajev.Main Hello World! There are 7 instances of class java.lang.Thread如果我添加一個線程t = new Thread(); 行到main方法,我在堆上看到8個實例。 聽起來好像真的可行。 您的線程數幾乎肯定會有所不同,不用擔心,這是正常現象,因為它確實計入了JVM簿記線程,進行編譯,GC等操作。
現在,如果我想計算堆上String實例的數量,只需更改參數類即可。 我希望圣誕老人是一個真正通用的解決方案。
哦,如果您有興趣,它會為我找到2423個String實例。 對于小型應用程序來說,這個數字相當高。 也,
return Thread.getAllStackTraces().size();給我5個而不是8個,因為它不包括簿記線程! 談論瑣碎的解決方案,是嗎?
現在,您已經掌握了這些知識,并且知道了本教程,并不是說您已經準備好編寫自己的JVM監視或增強工具,但這絕對是一個開始。
在本文中,我們從零開始編寫了本機Java代理,該代理成功編譯,加載和運行。 它使用JVMTI來獲取對JVM的了解,否則無法訪問。 相應的Java代碼調用本機庫并解釋結果。
這通常是最神奇的JVM工具所采用的方法,我希望其中的一些魔術已為您揭開神秘面紗。
翻譯自: https://www.javacodegeeks.com/2014/12/own-your-heap-iterate-class-instances-with-jvmti.html
總結
以上是生活随笔為你收集整理的拥有您的堆:使用JVMTI迭代类实例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring重试–与项目集成的方式
- 下一篇: 专利机构备案流程(专利机构备案)