管理堆空间:使用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