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

时间: 2024-09-17 00:47:51

Java开发中使用JVMTI循环类实例教程的相关文章

Android开发中RecyclerView控件使用实例教程

Android RecyclerView 是Android5.0推出来的,导入support-v7包即可使用. 个人体验来说,RecyclerView绝对是一款功能强大的控件. 首先总结下RecyclerView的特点: 1.支持不同方向,不同排版模式,实现多种展现数据的形式,涵盖了ListView,GridView,瀑布流等数据表现的形式 2.内部实现了回收机制,无需我们考虑View的复用情况 3.取消了onItemClick等点击事件,需要自己手动去写 -------------------

java开发中通用分页类代码

java开发中通用分页类代码 在java中要分页我们必须要有数据库教程,所以我们先准备下数据库,其数据库脚步如下: --以下是创建数据库和数据库表以及向数据库插入数据   use master  Go  if exists(select * from sysdatabases where name='pagination')  drop database pagination  Go  create database pagination  Go  use pagination  Go  cre

java开发中基于JDBC连接数据库实例总结_java

本文实例讲述了java开发中基于JDBC连接数据库的方法.分享给大家供大家参考,具体如下: 创建一个以JDBC连接数据库的程序,包含7个步骤:   1.加载JDBC驱动程序: 在连接数据库之前,首先要加载想要连接的数据库的驱动到JVM(Java虚拟机),这通过java.lang.Class类的静态方法forName(String  className)实现. 例如: try{ //加载MySql的驱动类 Class.forName("com.mysql.jdbc.Driver") ;

Java开发中的23种设计模式详解(转)

Java开发中的23种设计模式详解(转) 设计模式(Design Patterns)                                   --可复用面向对象软件的基础 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代码可靠性. 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样.项目中合

Filter组件开发中的SDK基类分析

DirectShow SDK提供了一套开发Filter的基类源代码.基于这些基类开发Filter将大大简化开发过程. 1.CBaseObject 大部分SDK类都从CBaseObject类(参见combase.h)中继承而来的. [cpp] view plaincopy class CBaseObject   {      private:          // Disable the copy constructor and assignment by default so you will

java spring注入bean生成一个类实例,请问这个类实例是单体类吗?全局唯一吗。

问题描述 java spring注入bean生成一个类实例,请问这个类实例是单体类吗?全局唯一吗. 小弟刚从C++转JAVA不久,遇到这样一个问题,求高人帮忙解答. 我现在大体理解了注入的实现方式,例如在一个标注有@configuration 的类里面,如果一个方法 上面有@bean,那么这个方法的返回的类对象会被实例化. 我的疑问是这样的,这个实例化的对象是全局唯一的吗,或者说 是一个单体类吗? 因为我要在我的程序里不同地方使用调用这个bean的方法,我担心如果是单体类的话, 是否存在数据同步

开放-java开发中,一般涉及到金钱计算的时候用什么数据类型?

问题描述 java开发中,一般涉及到金钱计算的时候用什么数据类型? java开发中,一般涉及到金钱计算的时候用什么数据类型?精度尽量高 解决方案 BigDecimal类把数字封装再进行运算就可以精确运算 解决方案二: 其实用integer或者long整体放大100倍就可以了.精确到1分钱.最大可以表示4200万(对于int),至于long,表示的更是天文数字了. 解决方案三: BigDecimal一般用在货币计算 解决方案四: BigDecimal 解决方案五: BigDecimal可以表示任意

mongodb的java开发中使用两个字段相等查询条件应该怎么写?

问题描述 mongodb的java开发中使用两个字段相等查询条件应该怎么写? 也就是类似sql中的where 字段A=字段B这样的条件,我查了下网上有些帖子中写的用$where的方式我查询的时候会出错,类似于:new Document("$where",new Document("字段A", "字段B")),驱动版本是3.0 在网上查到一个帖子:http://cache.baiducontent.com/c?m=9d78d513d99c1cf30

Java开发中方法调用出现 Java.Lang.NoSuchMethod 错误如何解决

Java开发中调用webservice,方法调用报了 Java.Lang.NoSucheMethod..........,印象中记得是jar包冲突,maven项目,一看,这一堆jar包...用eclipse的pom可视化界面,Dependency Hierarchy 看到了不少包冲突,先从webservice 依赖的jar开始,顺着这一思路,一顿排除,没管用. 网上搜了几篇文章,梳理了下解决问题的思路: 1.你调的类里缺少该方法,可能是由于jar包的版本不对 2.项目依赖的jar包,A包和B包有