管理堆空间:使用JVMTI循环类实例

y37f 9年前

今天我想探讨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