arthas icon indicating copy to clipboard operation
arthas copied to clipboard

Arthas vmtool源码分析

Open loongs-zhang opened this issue 2 years ago • 3 comments

Arthas vmtool源码分析

Hello JNI

Why use JNI ?

  • 提高程序性能;
  • 实现某些纯Java代码不可能实现的功能;
  • 使用其他语言的类库;
  • 与硬件、操作系统进行交互。

What is JNI ?

JNI是Java Native Interface的缩写,通过使用native关键字书写程序,允许Java与其他语言进行交互。

How to write application with JNI ?

step1.定义native方法

public class Main {

    public static native String helloJni();

}

step2.生成头文件

我们使用命令生成c语言使用的头文件

javac -h . Main.java
# 两个命令都可以,但是从JDK10开始javah被废弃
# 因此推荐使用上面的命令
javah Main

下面是生成头文件Main.h的具体内容:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class Main */

#ifndef _Included_Main
#define _Included_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     Main
 * Method:    helloJni
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_Main_helloJni
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

step3.编写native的实现MainImpl.c

#include <jni.h>
#include <jni_md.h>
#include <jvmti.h>
#include "Main.h"

JNIEXPORT jstring JNICALL Java_Main_helloJni
        (JNIEnv *env, jclass klass) {
    return env->NewStringUTF("Hello JNI");
}

step4.生成动态链接库

我的JAVA_HOME/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home,对应生成动态链接库的命令为:

g++ -I /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/include
-I /Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/include/darwin 
-I /Users/admin/Downloads/study/jni/src/main/native 
MainImpl.c -m64 -fPIC -shared -o jni.dylib

特别注意:

  • -I要包含JAVA_HOMEinclude文件夹下的全部文件夹,不同平台的include子文件夹不一样;
  • 如果是32位的操作系统,需要把命令中的-m64改为-m32
  • 不同平台生成的动态链接库后缀不同,比如linux是.so,mac是.dylib.so,windows是.dll

step5.加载动态链接库

java.lang.System#load

step6.调用native方法

直接像调用一个java方法一样调用它就好了,下面附上完整代码:

import java.net.URL;

public class Main {

    static {
        final URL url = Main.class.getResource("jni.dylib");
        System.load(url.getPath());
    }

    public static native String helloJni();

    public static void main(String[] args) {
        System.out.println(helloJni());
    }
}

诚如您所见,编写一个使用了JNI的Java程序并不难!

Generic JNI

JNI shortcoming

  • 使用Java与动态链接库交互,通常会丧失JVM平台的可移植性,这意味着要我们自己兼容不同的平台。

Compatible JNI

在vmtool正式贡献之前,我尝试了几种方案来生成动态链接库

  1. Runtime.getRuntime().exec("......")动态生成,失败,由于安全问题,此API在生产环境直接被禁用了;
  2. 交叉编译,失败,根本找不到Java生态可调用的api;
  3. 安装vmware并安装不同平台的虚拟机,然后在虚拟机上打不同的动态链接库,失败,真实原因由于个人水平有限不得而知,猜测是打包调用时最终会调用到底层的操作系统,而操作系统之间不互通;
  4. native-maven-plugin,成功,底层仍是使用RuntimeAPI,只是因为打包的机器没有禁用Runtime相关API,所以能成功;

Better JNI

JDK的坑

使用native-maven-plugin时需要配置JDK中包含头文件的目录名(对于Oracle JDK其实就是include),但是对于其他JDK可能就不是include目录了

怎么解决这个问题呢?

作者的做法是把不同平台的JDK都下一遍,再对它们的include文件夹做整合,最终才呈现给大家arthas-vmtool/src/main/native/head

警惕内存泄露

在vmtool最初的 PR 里,调用GetObjectsWithTagsGetLoadedClasses后没有释放内存的代码,这也就导致了必定发生的内存泄漏,提完 PR 后,我没有注意到部分代码存在本地方法栈内存泄漏(不了解的同学建议阅读周志明的《深入理解Java虚拟机》),幸亏 kylixs 发现并立刻通知,才让内存泄漏问题在vmtool正式发布之前被解决,在此鸣谢。

敏锐的读者可能已经察觉到了,不是调用所有的JVMTI方法都要编写释放内存的逻辑,那么调用JVMTI的哪些方法要编写呢?请参考JVMTI手册

干掉不必要的回调

最开始getInstances0的返回结果是List<T>而不是T[]

static native <T> List<T> getInstances0(Class<T> klass, int limit);

这意味着要在c的代码中回调java的java.util.ArrayList#add,当这种回调用达到一个量级后,能明显看到调用所耗费的时间。

作者记得之前跑一个benchmark花了5min,干掉不必要的回调、改成返回T[]后,再跑benchmark发现只耗费1min了,由此可见提升是多么地巨大。

Awesome vmtool

Analyze

前面铺垫了那么多,终于进入源码分析的正题了,我们以arthas.VmTool#getInstances0为例分析。

step1.初始化JVMTI

初始化JVMTI(后续遍历堆从堆中获取类实例释放内存都依赖于JVMTI,读者可以理解为JNI包含了JVMTI):

static jvmtiEnv *jvmti;

//这里的extern "C"是为了向下兼容C
extern "C"
int init_agent(JavaVM *vm, void *reserved) {
    //获取JVMTI
    jint rc = vm->GetEnv((void **)&jvmti, JVMTI_VERSION_1_2);
    if (rc != JNI_OK) {
        fprintf(stderr, "ERROR: arthas vmtool Unable to create jvmtiEnv, GetEnv failed, error=%d\n", rc);
        return -1;
    }
    //配置JVMTI
    jvmtiCapabilities capabilities = {0};
    capabilities.can_tag_objects = 1;
    jvmtiError error = jvmti->AddCapabilities(&capabilities);
    if (error) {
        fprintf(stderr, "ERROR: arthas vmtool JVMTI AddCapabilities failed!%u\n", error);
        return JNI_FALSE;
    }
    return JNI_OK;
}

//通过premain方式启动JavaAgent会回调此方法
extern "C" JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    return init_agent(vm, reserved);
}

//通过attach方式启动JavaAgent会回调此方法
extern "C" JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved) {
    return init_agent(vm, reserved);
}

//通过java.lang.System.load或者java.lang.System.loadLibrary动态加载动态链接库会回调此方法
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM* vm, void* reserved) {
    init_agent(vm, reserved);
    return JNI_VERSION_1_6;
}

step2.遍历堆

我们需要获取某个类的实例怎么办?遍历堆吧。

static LimitCounter limitCounter = {0, 0};

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
    //这里获取一个唯一标记
    jlong tag = getTag();
    //这里初始化计数器
    limitCounter.init(limit);
    //......
}

出于性能方面的考虑,VmTool#getInstances0默认只会获取JVM上某个类的10个实例,也就是说我们遍历堆,一旦发现已经有10个实例就没必要继续遍历了,那么怎么记录已遍历的实例数量呢?借助于arthas自定义的LimitCounter

struct LimitCounter {
    //已遍历过的实例数
    jint currentCounter;
    //需要的实例数,<0则表示需要堆中的所有实例
    jint limitValue;

    void init(jint limit) {
        currentCounter = 0;
        limitValue = limit;
    }

    void countDown() {
        currentCounter++;
    }

    bool allow() {
        if (limitValue < 0) {
            return true;
        }
        return limitValue > currentCounter;
    }
};

真正去遍历堆:

extern "C"
jvmtiIterationControl JNICALL
HeapObjectCallback(jlong class_tag, jlong size, jlong *tag_ptr, void *user_data) {
    //对符合要求的对象打上标记
    jlong *data = static_cast<jlong *>(user_data);
    *tag_ptr = *data;
    
    //已遍历的count数增加
    limitCounter.countDown();
    if (limitCounter.allow()) {
        //没到限制继续遍历
        return JVMTI_ITERATION_CONTINUE;
    } else {
        //超过限制就不遍历了
        return JVMTI_ITERATION_ABORT;
    }
}

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
    //......
    //遍历堆
    jvmtiError error = jvmti->IterateOverInstancesOfClass(klass, JVMTI_HEAP_OBJECT_EITHER,
                                               HeapObjectCallback, &tag);
    if (error) {
        printf("ERROR: JVMTI IterateOverInstancesOfClass failed!%u\n", error);
        return NULL;
    }
    //......
}

step3.从堆中获取已标记的实例

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
    //......
    jint count = 0;
    jobject *instances;
    error = jvmti->GetObjectsWithTags(1, &tag, &count, &instances, NULL);
    if (error) {
        printf("ERROR: JVMTI GetObjectsWithTags failed!%u\n", error);
        return NULL;
    }
    //......
}

step4.把获取到的实例添加到数组

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
    //......
    //创建一个数组
    jobjectArray array = env->NewObjectArray(count, klass, NULL);
    for (int i = 0; i < count; i++) {
        //添加元素到数组
        env->SetObjectArrayElement(array, i, instances[i]);
    }
    //......
}

step5.释放内存并返回结果

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_arthas_VmTool_getInstances0(JNIEnv *env, jclass thisClass, jclass klass, jint limit) {
    //......
    //释放内存
    jvmti->Deallocate(reinterpret_cast<unsigned char *>(instances));
    //返回结果
    return array;
}

Regret

唯一的、最大的遗憾就是vmtool模块不能单独使用,如果可以单独使用的话,vmtool在获取类实例上提供了远比Spring强大的功能(Spring只能获取由BeanFactory实例化的instance,而vmtool可以获取JVM级别的instance)。

loongs-zhang avatar Sep 22 '21 13:09 loongs-zhang

nice job!

sheepblueblue avatar Sep 25 '21 14:09 sheepblueblue

nice job!

3q

loongs-zhang avatar Oct 06 '21 13:10 loongs-zhang

image 其他的action 现在还不支持吗

GentleSong avatar Mar 30 '22 14:03 GentleSong

源码分析相关的可以打上相关label标签吗? 方便看

jdxia avatar Jan 10 '23 06:01 jdxia

vmtool对业务应用的性能上的影响有过测试数据之类的么,如果想把这个作为一个常用的监控,比如定时使用vmtool获取一些数据,是否建议

angjuLin avatar Jul 20 '23 12:07 angjuLin

vmtool对业务应用的性能上的影响有过测试数据之类的么,如果想把这个作为一个常用的监控,比如定时使用vmtool获取一些数据,是否建议

vmtool getInstances默认只会拿指定类的10个实例,控制好频率和调用量,对线上影响应该较小;

loongs-zhang avatar Jul 24 '23 13:07 loongs-zhang

👌

angjuLin avatar Aug 16 '23 06:08 angjuLin