Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arthas vmtool源码分析 #1920

Closed
loongs-zhang opened this issue Sep 22, 2021 · 7 comments
Closed

Arthas vmtool源码分析 #1920

loongs-zhang opened this issue Sep 22, 2021 · 7 comments

Comments

@loongs-zhang
Copy link
Contributor

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)。

@sheepblueblue
Copy link
Contributor

nice job!

@loongs-zhang
Copy link
Contributor Author

nice job!

3q

@GentleSong
Copy link

image

其他的action 现在还不支持吗

@jdxia
Copy link

jdxia commented Jan 10, 2023

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

@angjuLin
Copy link

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

@loongs-zhang
Copy link
Contributor Author

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

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

@angjuLin
Copy link

👌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants