JVMTI 用于Android 性能监控的4种基操和2个有趣的小结论

2,174 阅读6分钟

一、什么是JVMTI

JVMTI是用来开发和监控JVM所使用的程序接口,可以探查JVM内部状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:

  • 调试,断点
  • 监控内存分配,回收
  • 分析线程创建结束
  • 覆盖率分析
  • 堆栈管理
  • 字节码hook等

需要注意的是,并非所有的JVM实现都支持JVMTI,Android 是 8 以后才加入的JVMTI实现。

中文文档

blog.caoxudong.info/blog/2017/1…

英文文档

docs.oracle.com/javase/8/do…

原理图大概是这个样子,可以看成一个中间代理人

f936ccc76a648280facad34a06380eb3.png

二、为什么接触到JVMTI

因为在最近在研究Android性能监控方面的问题,无意之间接触到这个黑科技。

三、具体能干什么

JVMTI 就是JVM给开发者的后门,你可以用它实时检测JVM的运行情况,包括对象分配,垃圾回收,线程调度,实时调试等。

四、想用它做什么

1. 实时收集对象分配情况(包括对象分配的数量和大小)

2. 记录GC事件,帮助分析内存泄漏

3. 记录线程活动

4. 记录方法调用(在方法调用和退出的时候记录运行时间)

五、如何实现上述这些功能目标

JVM 是C写的,所以你想要监听的话,需要用C/C++写一个动态连接库,然后在运行的时候attch这个库, 这个库必须要有一个主回调函数作为JVMTI的入口,如下

extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options,void *reserved)

在这个入口函数中,定制你需要监听的事件,总共有32种事件回调,你可以按需定制,回调事件越多对jvm性能影响越大,比如内存分配事件随时都在发生,而且评率非常高可能1秒钟达到1000次,又比如方法调用频率每秒钟可能达到数10000次,如果使用不当的话,程序员自己写的代码可能只写了几个方法,但是framework回调通知达到了几万次,甚至数十万次,这个就失去了监控的意义。

以下是Demo中我的Agent_OnAttach 方法如下。


/**
 * Agent attch 回调
 */
extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options,void *reserved) {
    //VM 在这里赋值才有效,在onLoad方法里赋值,使用的时候变成了null
    LOGI("JVM Agent_OnLoad: %d ,pid: %d",globalVm,getpid());
    LOGI("JVM Agent_OnAttach: %d ,pid: %d",vm,getpid());
    ::globalVm=vm;

    JNIEnv *env;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    LOGI("Find helper class on onattch%s",JVM_TI_CLASS);
    LOGI("Classs Exist:%d", helperClass);

//    ::helperClass = env->FindClass(JVM_TI_CLASS);
    //================================================

    jvmtiEnv *jvmti_env = CreateJvmtiEnv(vm);

    if (jvmti_env == nullptr) {
        return JNI_ERR;
    }
    localJvmtiEnv = jvmti_env;
    SetAllCapabilities(jvmti_env);

    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    
    //设置回调函数
    callbacks.VMObjectAlloc = &onObjectAllocCallback;//绑定内存分配
    callbacks.NativeMethodBind = &JvmTINativeMethodBind;//

    callbacks.GarbageCollectionStart = &onGCStartCallback;//GC 开始
    callbacks.GarbageCollectionFinish = &onGCFinishCallback; //GC 结束

    callbacks.MethodEntry=&onMethodEntry;
    callbacks.MethodExit=&onMethodExit;

    callbacks.ThreadStart=&onThreadStart;
    callbacks.ThreadEnd=&onThreadEnd;


    int error = jvmti_env->SetEventCallbacks(&callbacks, sizeof(callbacks));

    //启用各种回调事件,否则可能不会触发回调方法
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_GARBAGE_COLLECTION_START);//监听GC 开始
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_GARBAGE_COLLECTION_FINISH);//监听GC 结束
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_NATIVE_METHOD_BIND);//监听native method bind
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_VM_OBJECT_ALLOC);//监听对象分配
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_OBJECT_FREE);//监听对象释放
    SetEventNotification(jvmti_env, JVMTI_ENABLE,JVMTI_EVENT_CLASS_FILE_LOAD_HOOK);//监听类文件加载
    SetEventNotification(jvmti_env,JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY);//方法进入
    SetEventNotification(jvmti_env,JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT);//方法退出

    SetEventNotification(jvmti_env,JVMTI_ENABLE, JVMTI_EVENT_THREAD_START);//线程开始
    SetEventNotification(jvmti_env,JVMTI_ENABLE, JVMTI_EVENT_THREAD_END);//线程结束

    LOGI("==========Agent_OnAttach=======");
    return JNI_OK;

}

完整源码我会放到最后

需要注意的是要得到回调通知,首先要保证以下两点:

  1. 设置回调方法
  2. 启用对应的监听目标

下面看看上述的四种需求如何实现

用途1.记录内存分配

如果你绑定了内存分配回调,绝大多数的内存创建都会通知到回调方法,其中大部分都是系统对象的创建通知,如果你想在这里统计属于自己的new出来的内存占用也不是不可以,但频繁的通知会耗费更多的性能,实在得不偿失,但好处就是在这里可以拦截几乎所有Java层的内存分配(attch之前分配的内存拿不到)

  开启 JVMTI_EVENT_GARBAGE_COLLECTION_START 的监听  

 设置回调函数
  callbacks.VMObjectAlloc = &onObjectAllocCallback;//绑定内存分配

这样所有的内存分配都会通知到onObjectAllocCallback函数

用途2.记录GC事件

JVMTI_EVENT_GARBAGE_COLLECTION_START 和JVMTI_EVENT_GARBAGE_COLLECTION_FINISH 则是GC开始和GC结束的回调事件,结合这2个事件对内存泄漏还是有帮助,而且这2个事件产生的频率并不高,我觉得还是有比较实在的用途


    开启JVMTI_EVENT_GARBAGE_COLLECTION_START和JVMTI_EVENT_GARBAGE_COLLECTION_FINISH监听

    设置回调函数
    
    callbacks.GarbageCollectionStart = &onGCStartCallback;//GC 开始
    callbacks.GarbageCollectionFinish = &onGCFinishCallback; //GC 结束

当注册好这2个事件,就可以在onGCStartCallback 和 onGCFinishCallback 这2个方法中收到GC回调了

用途:3.记录方法调用

在为 jvmtiEventCallbacks 设置好回调方法且启用对应的监听之后,当事件发生后就会收到jvm的回调事件。 比如我设置了MethodEntry和MethodExit的监听,那么当程序启动后,就会收到洪水般的回调消息

    开启JVMTI_EVENT_METHOD_ENTRY 和 JVMTI_EVENT_METHOD_EXIT监听
    
    设置回调函数
    
    callbacks.MethodEntry=&onMethodEntry;
    callbacks.MethodExit=&onMethodExit;

你可以在onMethodEntry方法中获取方法执行的的开始事件,在onMethodExit中获取方法执行的结束事件,从而计算出方法的执行时间,但是这种方法实在鸡肋了,因为jvmti是站在应用层和jvm中间的,所有jvm活动都会通知到回调方法中,其中绝大多数都是framework层的方法,这样太过于低效了,实在不适合用来监听方法调用(编译时插桩按需定制监听的方法更简单高效)。

用途4.记录线程的开始和结束

由于线程的创建相对没有那么密集,用这种方式统计线程的使用情况相对合理


    开启JVMTI_EVENT_THREAD_START和JVMTI_EVENT_THREAD_END 监听
    
    设置回调函数
    
    callbacks.ThreadStart=&onThreadStart;
    callbacks.ThreadEnd=&onThreadEnd;

你可以在onThreadStart和onThreadEnd 这2个自定义的回调方法中坚挺到线程的创建和结束

六、2个有趣的小结论是什么

1.从App启动到开启第一个只有2个按钮页的Activity执行的方法居然达到11万之多

在这个Demo执行的程序员自定义的方法不会超过10个,所以这个比例还是挺离谱的,用这种方式来计算方法执行时间意义不大。

%}$08KF1ZE)9D8S84S{)9TB.png

2.从App启动到一个简单的Activity 页面开启,需要分配的对象个数大概是7500个左右

这个简单的Demo中程序员自己new的对象不超过10个

image.png

当然上述统计非常粗糙,不是标准知识,而是卷人的数据...

七、我遇到的问题(也许是我的问题)

1.1 在回调方法中无法加载程序员自定义的class

无法加载程序员自定的class,自然无法调用自定义Java层的方法,我想在回调方法通知到Java层,但很遗憾无法加载自定义的类,只能加载framework的和javaselib的类(说是jvmti有限制),所以要想更好的统计数据,需要写更多的C代码了

1.2 我在方法MethodEntry执行的时候想打印方法所在的类名,但是必须调用Class 的getName方法,但是这个方法是Java层的,所以就造成死递归,我想通过方法名排除掉,但是很遗憾,似乎看到执行效果,调试也无法单步进入

1.3 Debug 经常不工作(感觉有点鸡肋)

1.4 无法保存class 和 javavm的全局变量(这个可能是我的问题)

1.5 只能用于Debug阶段(但经过大佬的hack貌似也可用于release阶段,说实话,release阶段用这个东西,并不明智)

八.完整源码

github.com/woshiwzy/My…

JVMTI 功能强大,我利用的只是冰山一角,以下是我参考过的大佬文章

九、参考文献

blog.csdn.net/duqi_2009/a…

blog.csdn.net/z1032689332…

blog.csdn.net/zhuoxiuwu/a…