管理堆空间:使用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
[ 转载请保留原文出处、译者和译文链接。]



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部