转载

管理堆空间:使用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工具经常采用的策略,我希望我已经为你解释清楚了其中的一些技巧。

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

原文链接: JavaCodeGeeks 翻译:ImportNew.com -文 学敏

译文链接:[

]

正文到此结束
Loading...