[alibaba/arthas]Arthas vmtool源码分析

2024-07-09 530 views
6
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语言使用的`头文件`。
```shell
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是.dllstep5.加载动态链接库
    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`,成功,底层仍是使用`Runtime`API,只是因为打包的机器没有禁用`Runtime`相关API,所以能成功;

## Better JNI
### JDK的坑
使用`native-maven-plugin`时需要配置JDK中包含头文件的目录名(对于`Oracle JDK`其实就是`include`),[但是对于其他`JDK`可能就不是`include`目录了](http://github.com/alibaba/arthas/pull/1698#issuecomment-785887313) 。

怎么解决这个问题呢?

作者的做法是把不同平台的JDK都下一遍,再对它们的`include`文件夹做整合,最终才呈现给大家`arthas-vmtool/src/main/native/head`。
### 警惕内存泄露
在vmtool最初的 [PR](https://github.com/alibaba/arthas/pull/1698) 里,调用`GetObjectsWithTags`、`GetLoadedClasses`后没有`释放内存`的代码,这也就导致了`必定`发生的`内存泄漏`,提完 [PR](https://github.com/alibaba/arthas/pull/1698) 后,我没有注意到`部分代码`存在`本地方法栈内存泄漏`(不了解的同学建议阅读周志明的《深入理解Java虚拟机》),幸亏 [kylixs](https://github.com/kylixs) 发现并立刻通知,才让内存泄漏问题在vmtool正式发布之前被解决,在此鸣谢。

敏锐的读者可能已经察觉到了,`不是`调用所有的JVMTI方法都要编写释放内存的逻辑,那么调用JVMTI的哪些方法要编写呢?请参考[JVMTI手册](https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html) 。
### 干掉不必要的回调
最开始`getInstances0`的返回结果是`List<T>`而不是`T[]`:
```java
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)。

回答

4
image

其他的action 现在还不支持吗

9

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

5

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

8

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