本文内容

       1.JVMTI与JNI

       2.统计Java类对象实例的用途和意义

       3.结合代码通过JVMTI实现对JVM堆中类对象实例个数的统计

前言

         Java没有提供很直接优雅的方式让我们能够去轻易的去获取类对象的实例的数量,也许是出于性能的考虑,也许是这种事情本身没有显示出多大的价值,毕竟不那么常用。虽然没有提供直接的方式,但是间接的方式还是有的。我们可以借助JVMTI,JVMTI是 Java Virtual Machine Tool Interface 的缩写,也即Java虚拟机调试接口,可以认为这是设计者预留的后门,它的功能十分强大。虽然Java的调试功能不一定都是直接使用JVMTI来实现,但是却可以使用JVMTI来实现相应的调试功能。

          相比于JVMTI,JDI也是实现Java调试的一种实现。不过它是一种更高级的被封装的接口,它和JVMTI的区别在于JDI可以完全使用Java语言来实现自己的调试工具。基于JNI的JVMTI就显得更底层一点,对细节的把控能力更强。JDK安装目录下的 lib\tools.jar文件中包含了JDI的实现,基于JNI的JVMTI的接口调用则需要使用c/c++代码来控制。如果把调试比作割麦子,那么使用JDI的话可以认为是用机器割麦子,使用JVMTI的话就是手工用镰刀割麦子。那么显而易见,手工控制虽然耗时,但是灵活性更高,控制性更强,什么样的田都可以割。机器的话虽然快,但是它的适用场景有限。

         JNI 是 Java Native Interface的缩写。它是Java接口与本地接口之间的桥梁,能够实现Java代码与本地其它语言(一般是c/c++)实现的代码进行交互,只需要保证两者按照约定的规范调用即可。在java中使用 native 关键字的修饰的方法被标记为本地方法,方法签名使用java语言声明,方法体逻辑一般由c/c++实现,c/c++编译后的本地方法以dll文件的形式存在。在Java中使用 System.load或者System.loadLibrary 载入dll的路径以完成java对c++的调用,而c++对java的调用则是根据 jvm提供的 JNIEnv和JVMTIEnv环境指针来操作具体的API接口。

         统计Java类对象实例个数能够帮助我们分析JVM运行中的类实例对内存的占用情况,如果碰到类似内存溢出或者内存泄漏的异常,可以找到是哪些类引发的问题。实际上,JDK工具集中的 jvisualvm.exe已经包含了堆转储的的功能,其中就包括对每个类的实例个数统计情况

       

但是从实际角度出发,我们还是有必要亲自动手实践一下,以助于灵活运用。 下面我们编写例子程序来细化这个过程,使用Java使用JNI调用DLL统计出类的实例数量。

 

一、使用Java编写测试类并生成头文件

        1.定义了一个测试类JVMTIBinder,并实例化了该类的三个静态变量binder1,binder2,binder3,并定义了一个native接口followReferences,该接口包含一个Class类型参数,返回值类型为ClassInfo数组类型 

    

     ClassInfo对象 中各字段含义分别为:

                cls表示被虚拟机装载的Class实例;

               count表示该Class实例对象的数量,也就是该Class被new过几次;

               size表示该Class占用空间的大小;

               objects 表示 该Class实例对象的引用集合

     那么上述followReferences方法的作用是传入一个Class参数。当Class参数值为空的时候,返回虚拟机中已装载的类的统计信息集合,当Class参数值不为空的时候,返回该Class的统计信息。

完整代码如下:

package com.suntown.jvmti;

import java.util.HashMap;
import java.util.Map;

public class JVMTIBinder{
    static JVMTIBinder binder1 = new JVMTIBinder("a11");
    static JVMTIBinder binder2 = new JVMTIBinder("a22");
    static JVMTIBinder binder3 = new JVMTIBinder("a33");

    private String name;
    public JVMTIBinder(String a6) {
        this.name = a6;
    }

    public static class ClassInfo{
        public Class cls;
        public int count;
        public int size;
        public Object[] objects;
    }

    public static void main(String[] args) throws ClassNotFoundException{
        ClassInfo[] cis = followReferences(JVMTIBinder.class);
        for(ClassInfo c : cis){
            System.out.println("classname=>"+c.cls.getName()+"\tcount=>" + c.count +"\tsize=>" + c.size+"\tobject.length=>"+c.objects.length);
            for(Object o : c.objects) {
                if (o instanceof JVMTIBinder) {
                    JVMTIBinder jo = (JVMTIBinder) o;
                    System.out.println(jo.name);
                }
            }
        }
    }

    public static native ClassInfo[] followReferences(Class cls);

    static{
        System.setProperty("printCapabilities","0");
        System.load("D:\\Users\\DELL\\source\\repos\\JVMTIBinder\\x64\\Release\\JVMTIBinder.dll");
    }
}

2.通过JDK工具javah生成native方法的头文件

    定位到JVMTIBinder类生成路径,使用 javah -jni com.suntown.jvmti.JVMTIBinder命令生成

对应的头文件 

        

打开com_suntown_jvmti_JVMTIBinder.h头文件可以看到 该头文件中声明了一个

      Java_com_suntown_jvmti_JVMTIBinder_followReferences的导出函数,该头文件引用了jni.h

 以jdk1.8.0_181为例,jni.h位于 jdk安装目录下的include文件夹下,同样的jvmti.h头文件也在这个目录下

 

二、使用c++编写本地方法代码

     使用vs创建一个动态链接库项目,创建一个源文件main.cpp

     在项目属性设置中包含目录中加入jdk的头文件目录

 并且在配置属性->C/C++->代码生成中 将启用C++异常项设置为 下图中选项,

该选项能够保证在代码中使用try catch捕获异常时生效

        

使用 System.loadLibrary加载dll的时候首先将会首先进到dll的JNI_OnLoad方法中,

在这个方法中,你可以

     1.获取 JNIENV环境指针

     

 

     2.获取JVMTIENV环境指针并且获取JVMTI可用的能力

        

          jvmtiCapabilities结构中包含jvmti所支持的能力,GetPotentialCapabilities方法获取jvmti在当前环境下虚拟机支持的能力,使用AddCapabilities方法使设置的能力属性生效。

        3.实现native接口followReferences的本地方法

           首先通过jvmtienv指针的GetLoadedClasses方法获取虚拟机已加载的Class实例集合,GetLoadedClasses声明如下

          

         然后遍历类对象集合并通过调用jvmtienv指针的SetTag方法给每个Class对象打标记,SetTag方法签名如下

         

          接着通过调用jvmtienv指针FollowReferences方法开始遍历Class实例对象的引用,该方法接受一个回调   jvmtiHeapCallbacks,该结构是一系列回调函数的集合。

         

        上述回调函数的第三个参数class_tag可以用来进行对Class进行分组,相同的class_tag对应相同的类对象,从而在FollowReferences的回调函数中完成Class对象实例个数的累加和所占用空间大小的计算。

        该函数的第二参数kclass为空的时候,会遍历所有已加载Class实例对象,不为空时只遍历该class实例对象。该方法调用结束时,在回调函数也已经完成了每个类对象的统计任务。

    最后,可以调用 jvmtienv指针的 IterateThroughHeap方法可以遍历Class实例对象树,遍历完成后可以通过jvmtienv指针的GetObjectsWithTags方法获取指定Class对象的引用集合。两个方法签名如下

     

 完整代码如下

     

 

 

 

 

三、运行测试 

       在Idea中运行 com.suntown.jvmti.JVMTIBinder类,运行结果如下图

      

      图中输出了 JVMBinder类实例的个数与空间大小,以及3个JVMBinder 类static静态变量的属性值,实现了对com.suntown.jvmti.JVMBinder类实例统计信息的输出。

      

 

    

       

 

       

      

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐