Android性能优化之JVMTI与内存分配
作者:Pika
前言
内存治理一直是每个开发者最关心的问题,我们在日常开发中会遇到各种各样的内存问题,比如OOM,内存泄露,内存抖动等等,这些问题都有以下共性:
- 难发现,内存问题一般很难发现,业务开发中关系系数更少
- 治理困难,内存问题治理困难,比如oom,往往堆栈只是压死骆驼的最后一根稻草
- 易复发,几乎没有一种方案,能够杜绝内存问题,比如内存泄露几乎是100%存在,只是不同项目影响的范围不同而已
内存问题目前经过业内多年沉淀以及开发,已经有很多方案了,比如检查内存泄露(LeakCanary,MIT,KOOM等)。相关文章已经有很多,所以我们从另一个角度出发,虚拟机侧有没有想过的方案检测内存呢?有的,那就是JVMTI(Java Virtual Machine Tool Interface)即指 Java 虚拟机工具接口,它是一套由虚拟机直接提供的 native 接口,我们可以从这里面获取虚拟机运行时的大部分信息。
友情提示:本文涉及native c层的代码,如果读者不熟悉也没关系,已经尽量减少相关的代码阅读成本啦!冲就对啦!JVMTI在debug模式下有很多用处,当然release环境也可以通过hook方式开启,但是不太建议,虽然jvmti有诸多限制,但是不妨碍我们多了解一个“黑科技”
JVMTI
JVMTI 简介:
JVMTI,即由java虚拟机提供的面向虚拟机接口的一套监控api,虽然虚拟机中一直存在,但是在android中是在Android 8.0(API 级别 26)或更高版本的设备上才正式支持。jvmti的功能本质就是“埋点化”,把jvm的一些事件通过“监听”的方式暴露给外部开发调试
jvmti监听的事件包包含了虚拟机中线程、内存、堆、栈、类、方法、变量,事件、定时器,锁等创建销毁相关事件,本次我们从实战的角度出发,看看如何实现一次内存分配的监听。
native层开启jvmti
前置准备
使用jvmti之前,我们需要创建一个native工程,同时我们需要使用jvmti的api,在native中就是头文件了,我们需要复制一份jdk中的名叫jvmti.h的头文件(在我们安装的jdk/include目录下),到我们的项目cpp根目录即可
此时我们也自定义一个memory.cpp作为我们使用jvmti的函数载体。jvmti.h里面包含了我们所需要的一切函数定义与常量,当然,这个头文件并不需要随着native工程进行打包,因为在真正使用到jvmti相关的工具时,是由系统进行so依赖查找进行定位的,该so位于系统库中(libopenjdkjvmtid.so、libopenjdkjvmti.so),所以我们不用关心具体的实现,接下来我们按照步骤进行即可,包括native层与java层
复写Agent
作为第一步,我们需要复写jvmti.h中的
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
这个是jvmti中的agent初始化的时候,由native回调,在这里我们可以拿到JavaVM环境,同时可以创建jvmtiEnv对象,该对象非常重要,用于native进行接下来的各种监听处理
// 全局的jvmti环境变量 jvmtiEnv *mJvmtiEnv;
extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options, void *reserved) { //准备JVMTI环境,初始化mJvmtiEnv vm->GetEnv((void **) &mJvmtiEnv, JVMTI_VERSION_1_2); return JNI_OK; }
开启jvmtiCapabilities
默认时,jvmti中是不提供任何能力给我们使用的,我们可以通过jvmtiEnv,去查询当前虚拟机实现的哪几种jvmti回调
jvmtiError GetPotentialCapabilities(jvmtiCapabilities* capabilities_ptr) { return functions->GetPotentialCapabilities(this, capabilities_ptr); } jvmtiError AddCapabilities(const jvmtiCapabilities* capabilities_ptr) { return functions->AddCapabilities(this, capabilities_ptr); }
可以看到,我们只需要传入一个jvmtiCapabilities对象指针即可,之后的能力数据就会被填充到该对象,所以我们接下来在Agent_OnAttach函数中继续补充以下代码
//初始化工作 extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options, void *reserved) { //准备JVMTI环境,初始化mJvmtiEnv vm->GetEnv((void **) &mJvmtiEnv, JVMTI_VERSION_1_2); //开启JVMTI的能力:到这一步啦!! jvmtiCapabilities caps; mJvmtiEnv->GetPotentialCapabilities(&caps); mJvmtiEnv->AddCapabilities(&caps); __android_log_print(ANDROID_LOG_ERROR, "hello", "Agent_OnAttach"); return JNI_OK; }
设置jvmtiEventCallbacks
我们已经查询到了jvmti所支持的回调,这个时候就到了正式设置回调的环节,jvmti中支持以下几种回调类型
typedef struct { /* 50 : VM Initialization Event */ jvmtiEventVMInit VMInit; /* 51 : VM Death Event */ jvmtiEventVMDeath VMDeath; /* 52 : Thread Start */ jvmtiEventThreadStart ThreadStart; /* 53 : Thread End */ jvmtiEventThreadEnd ThreadEnd; /* 54 : Class File Load Hook */ jvmtiEventClassFileLoadHook ClassFileLoadHook; /* 55 : Class Load */ jvmtiEventClassLoad ClassLoad; /* 56 : Class Prepare */ jvmtiEventClassPrepare ClassPrepare; /* 57 : VM Start Event */ jvmtiEventVMStart VMStart; /* 58 : Exception */ jvmtiEventException Exception; /* 59 : Exception Catch */ jvmtiEventExceptionCatch ExceptionCatch; /* 60 : Single Step */ jvmtiEventSingleStep SingleStep; /* 61 : Frame Pop */ jvmtiEventFramePop FramePop; /* 62 : Breakpoint */ jvmtiEventBreakpoint Breakpoint; /* 63 : Field Access */ jvmtiEventFieldAccess FieldAccess; /* 64 : Field Modification */ jvmtiEventFieldModification FieldModification; /* 65 : Method Entry */ jvmtiEventMethodEntry MethodEntry; /* 66 : Method Exit */ jvmtiEventMethodExit MethodExit; /* 67 : Native Method Bind */ jvmtiEventNativeMethodBind NativeMethodBind; /* 68 : Compiled Method Load */ jvmtiEventCompiledMethodLoad CompiledMethodLoad; /* 69 : Compiled Method Unload */ jvmtiEventCompiledMethodUnload CompiledMethodUnload; /* 70 : Dynamic Code Generated */ jvmtiEventDynamicCodeGenerated DynamicCodeGenerated; /* 71 : Data Dump Request */ jvmtiEventDataDumpRequest DataDumpRequest; /* 72 */ jvmtiEventReserved reserved72; /* 73 : Monitor Wait */ jvmtiEventMonitorWait MonitorWait; /* 74 : Monitor Waited */ jvmtiEventMonitorWaited MonitorWaited; /* 75 : Monitor Contended Enter */ jvmtiEventMonitorContendedEnter MonitorContendedEnter; /* 76 : Monitor Contended Entered */ jvmtiEventMonitorContendedEntered MonitorContendedEntered; /* 77 */ jvmtiEventReserved reserved77; /* 78 */ jvmtiEventReserved reserved78; /* 79 */ jvmtiEventReserved reserved79; /* 80 : Resource Exhausted */ jvmtiEventResourceExhausted ResourceExhausted; /* 81 : Garbage Collection Start */ jvmtiEventGarbageCollectionStart GarbageCollectionStart; /* 82 : Garbage Collection Finish */ jvmtiEventGarbageCollectionFinish GarbageCollectionFinish; /* 83 : Object Free */ jvmtiEventObjectFree ObjectFree; /* 84 : VM Object Allocation */ jvmtiEventVMObjectAlloc VMObjectAlloc; } jvmtiEventCallbacks;
我们需要监听的是内存分配与销毁的监听即可,分别是VMObjectAlloc与ObjectFree,在jvmtiEventCallbacks设定我们想要监听的事件之后,我们可以通过jvmtiEnv->SetEventCallbacks方法设定即可,所以我们可以继续在Agent_OnAttach中补充以下代码
jvmtiEventCallbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.VMObjectAlloc = &objectAlloc; callbacks.ObjectFree = &objectFree; //设置回调函数 mJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
其中objectAlloc是我们自定义的监听处理函数,如果jvm执行内存分配事件,就会回调此函数,该函数定义是
typedef void (JNICALL *jvmtiEventVMObjectAlloc) (jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jobject object, jclass object_klass, jlong size);
所以我们自定义的回调函数也要根据此定义进行编写。因为这里会回调所有java层的对象创建事件,回调次数非常多,在实际中我们可能并不关心系统类是如何分配内存的,而是关心我们自己的项目中的类的内存情况,所以这里我们做一个过滤,只有是项目的类我们才进行记录
void JNICALL objectAlloc(jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread, jobject object, jclass object_klass, jlong size) { jvmti_env->SetTag(object, tag); tag+= 1; char *classSignature; // 获取类签名 jvmti_env->GetClassSignature(object_klass, &classSignature, nullptr); // 过滤条件 if(strstr(classSignature, "com/test/memory") != nullptr){ __android_log_print(ANDROID_LOG_ERROR, "hello", "%s",classSignature); myVM->AttachCurrentThread( ¤tEnv, nullptr); // 这个list我们之后解释 list.push_back(tag); char str[500]; char *format = "%s: object alloc {Tag:%lld} \r\n"; sprintf(str, format, classSignature, tag); memoryFile->write(str, sizeof(char) * strlen(str)); } jvmti_env->Deallocate((unsigned char *) classSignature); }
我们可以看到,我们在中间做了一个jvmti_env->SetTag的操作,这个是给这个分配的对象进行了一个打标签的动作(我们需要观察该对象是否被销毁,所以需要一个唯一标识符),我们会在释放的时候用到。因为回调的操作可能会有很多,我们采用普通的io必定会导致native层的阻塞,所以这里就要靠我们的mmap登场了,通过mmap我们可以高效的处理频繁的io,mmap不熟悉的可以看这篇,memoryFile->write是一个通过mmap的写文件操作。
objectFree是我们的释放内存的监听,它的函数定义是
typedef void (JNICALL *jvmtiEventObjectFree) (jvmtiEnv *jvmti_env, jlong tag);
可以看到,我们在释放内存的时候得到的信息非常有限,只有一个tag,也就是我们在分配内存时通过SetTag操作所得到的参数,如果有设置就就会为具体的tag数值。我们在这个函数中的业务逻辑就是记录当次的释放记录即可
void JNICALL objectFree(jvmtiEnv *jvmti_env, jlong tag) { std::list<int>::iterator it = std::find(list1.begin(), list1.end(), tag); if (it != list.end()) // 找到了 { __android_log_print(ANDROID_LOG_ERROR, "hello", "release %lld",tag); char str[500]; char *format = "release tag %lld\r\n"; //ALOGI(format, GetCurrentSystemTime().c_str(),threadInfo.name, classSignature, size, tag); sprintf(str, format,tag); memoryFile->write(str, sizeof(char) * strlen(str)); } }
我们再回到上述代码留下的疑问,list是个什么?其实就是记录了我们在VMObjectAlloc阶段所分配的属于我们自定义的类的tag,因为ObjectFree提供给我们的信息非常有限,只有一个tag,如果不通过这个list保存分配内存时的tag的话,就会导致释放的时候我们引入过多的不必要的释放记录。但是这里也带来了一个问题,就是我们需要时刻同步list的状态,因为jvmti是可以在多线程环境下回调,如果只是简单操作list的话就会带来同步问题(这里我们没有处理,为了demo的简单)真实操作上我们最好加入mutex锁或者其他机制保证同步问题。
下面我们再给出memoryFile->write的代码
currentSize 记录当前大小 m_size 以页为单位的默认大小 void MemoryFile::write(char *data, int dataLen) { mtx.lock(); if(currentSize + dataLen >= m_size){ resize(currentSize+dataLen); } memcpy(ptr + currentSize, data, dataLen); currentSize += dataLen; mtx.unlock(); } void MemoryFile::resize(int32_t needSize) { // 如果mmap的大小不够,就需要重新进行mmap操作,以页为单位 int32_t oldSize = m_size; do{ m_size *=2; } while (m_size<needSize); ftruncate(m_fd, m_size); munmap(ptr, oldSize); ptr = static_cast<int8_t *>(mmap(0,m_size,PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0)); }
开启监听
到这里,我们还没有结束,我们需要真正的开启监听,前面只是设置监听的操作,我们可以通过SetEventNotificationMode函数开启真正监听/关闭监听
jvmtiError SetEventNotificationMode(jvmtiEventMode mode, jvmtiEvent event_type, jthread event_thread, ...) { return functions->SetEventNotificationMode(this, mode, event_type, event_thread); }
mode代表当前状态,是个枚举,event_type就是我们要开启监听的类型(这里我们指定为内存分配与释放事件即可),event_thread可以指定某个线程的内存分配事件,null就是全局监听,所以我们的业务代码如下
//开启监听 mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, nullptr); mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_OBJECT_FREE, nullptr);
java层开启agent
通过在native层设置了jvmti的监听与实现,我们还要在java层通过Debug.attachJvmtiAgent(9.0)进行开启,这里有细微差距
import android.content.Context import android.os.Build import android.os.Debug import android.util.Log import java.io.File import java.nio.file.Files import java.nio.file.Paths import java.util.* object MemoryMonitor { private const val JVMTI_LIB_NAME = "libjvmti-monitor.so" fun init(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //查找SO的路径 val libDir: File = File(context.filesDir, "lib") if (!libDir.exists()) { libDir.mkdirs() } //判断So库是否存在,不存在复制过来 val libSo: File = File(libDir, JVMTI_LIB_NAME) if (libSo.exists()) libSo.delete() val findLibrary = ClassLoader::class.java.getDeclaredMethod("findLibrary", String::class.java) val libFilePath = findLibrary.invoke(context.classLoader, "jvmti-monitor") as String Files.copy( Paths.get(File(libFilePath).absolutePath), Paths.get( libSo.absolutePath ) ) //加载SO库 val agentPath = libSo.absolutePath System.load(agentPath) //agent连接到JVMTI attachAgent(agentPath, context.classLoader); val logDir = File(context.filesDir, "log") val path = "${logDir.absolutePath}/test.log" initMemoryCallBack(path) } else { Log.e("memory", "jvmti 初始化异常") } } //agent连接到JVMTI private fun attachAgent(agentPath: String, classLoader: ClassLoader) { //Android 9.0+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { Debug.attachJvmtiAgent(agentPath, null, classLoader) } else { //android 9.0以下版本使用反射方式加载 val vmDebugClazz = Class.forName("dalvik.system.VMDebug") val attachAgentMethod = vmDebugClazz.getMethod("attachAgent", String::class.java) attachAgentMethod.isAccessible = true attachAgentMethod.invoke(null, agentPath) } } // 设置mmap的文件path external fun initMemoryCallBack(path: String) }
attachJvmtiAgent方法需要实现了jvmti 的so库的绝对地址,那么我们如何查找一个so库的地址呢?其实就是通过ClassLoader的findLibrary方法,我们可以获取到so的绝对地址,不过这个绝对地址不能够直接用,我们看一下源码attachJvmtiAgent
public static void attachJvmtiAgent(@NonNull String library, @Nullable String options, @Nullable ClassLoader classLoader) throws IOException { Preconditions.checkNotNull(library); Preconditions.checkArgument(!library.contains("=")); if (options == null) { VMDebug.attachAgent(library, classLoader); } else { VMDebug.attachAgent(library + "=" + options, classLoader); } }
其中attachJvmtiAgent 会进行格式校验Preconditions.checkArgument(!library.contains("=")),恰好我们得到的so的地址是包含=的,所以才需要一个File的copy操作(拷贝到一个不包含=的目录下)
验证分配数据
通过上面的jvmti操作,我们已经可以将数据保存到本地文件了,本地文件的保存可以自己定义,这里我保存在context.filesDir目录中/log子目录下,同时我们生成一个测试数据
package com.test.memory data class TestData(val test:Int) { }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.sampleText.text = "Hello World" TestData(1) }
运行后
我们就完成了一个内存的记录,通过该记录我们就能够分析哪些类引起了内存问题(即存在分配tag不存在释放tag)
总结
到这里,我们终于完成了一个jvmti的监控操作!当然,上面的代码还有很多需要提升的地方,比如多线程引用,比如我们可以同时开启MethodEntry的callback记录一个方法的开始和结束,为内存泄漏的定位做更加详细的分析等等!因为篇幅有限,这里就当作拓展留给读者们自行实现啦,以上就是Android性能优化之JVMTI与内存分配的详细内容,更多关于Android性能JVMTI内存分配的资料请关注脚本之家其它相关文章!