一、什么是JVMTI
JVMTI是用来开发和监控JVM所使用的程序接口,可以探查JVM内部状态,并控制JVM应用程序的执行。可实现的功能包括但不限于:
- 调试,断点
- 监控内存分配,回收
- 分析线程创建结束
- 覆盖率分析
- 堆栈管理
- 字节码hook等
需要注意的是,并非所有的JVM实现都支持JVMTI,Android 是 8 以后才加入的JVMTI实现。
中文文档
https://blog.caoxudong.info/blog/2017/12/07/jvmti_reference#1.1
英文文档
https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#SpecificationIntro
原理图大概是这个样子,可以看成一个中间代理人
二、为什么接触到JVMTI
因为在最近在研究Android性能监控方面的问题,无意之间接触到这个黑科技。
三、具体能干什么
JVMTI 就是JVM给开发者的后门,你可以用它实时检测JVM的运行情况,包括对象分配,垃圾回收,线程调度,实时调试等。
四、我想用它做什么
- 实时收集对象分配情况(包括对象分配的数量和大小)
- 记录GC事件,帮助分析内存泄漏
- 记录线程活动
- 记录方法调用(在方法调用和退出的时候记录运行时间)
五、如何实现上述这些功能目标
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.记录内存分配
如果你绑定了内存分配回调,绝大多数的内存创建都会通知到回调方法,其中大部分都是系统对象的创建通知,如果你想在这里统计属于自己的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个,所以这个比例还是挺离谱的,用这种方式来计算方法执行时间意义不大。
2.从App启动到一个简单的Activity 页面开启,需要分配的对象个数大概是7500个左右
这个简单的Demo中程序员自己new的对象不超过10个
当然上述统计非常粗糙,不是标准知识,而是卷人的数据…
七、我遇到的问题(也或许是我的问题)
- 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阶段用这个东西,并不明智)
八.完整源码
https://github.com/woshiwzy/MyAndroidJVMTIDEmo
JVMTI 功能强大,我利用的只是冰山一角,以下是我参考过的大佬文章
九、参考文献
https://blog.csdn.net/duqi_2009/article/details/94518203
https://blog.csdn.net/z1032689332/article/details/104477182?spm=1001.2101.3001.6650.2&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.pc_relevant_default&utm_relevant_index=5
https://blog.csdn.net/zhuoxiuwu/article/details/118694396?spm=1001.2101.3001.6650.6&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-6.pc_relevant_paycolumn_v3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7ERate-6.pc_relevant_paycolumn_v3&utm_relevant_index=9
|