Android性能優(yōu)化之JVMTI與內(nèi)存分配
前言
內(nèi)存治理一直是每個(gè)開發(fā)者最關(guān)心的問題,我們?cè)谌粘i_發(fā)中會(huì)遇到各種各樣的內(nèi)存問題,比如OOM,內(nèi)存泄露,內(nèi)存抖動(dòng)等等,這些問題都有以下共性:
- 難發(fā)現(xiàn),內(nèi)存問題一般很難發(fā)現(xiàn),業(yè)務(wù)開發(fā)中關(guān)系系數(shù)更少
- 治理困難,內(nèi)存問題治理困難,比如oom,往往堆棧只是壓死駱駝的最后一根稻草
- 易復(fù)發(fā),幾乎沒有一種方案,能夠杜絕內(nèi)存問題,比如內(nèi)存泄露幾乎是100%存在,只是不同項(xiàng)目影響的范圍不同而已
內(nèi)存問題目前經(jīng)過業(yè)內(nèi)多年沉淀以及開發(fā),已經(jīng)有很多方案了,比如檢查內(nèi)存泄露(LeakCanary,MIT,KOOM等)。相關(guān)文章已經(jīng)有很多,所以我們從另一個(gè)角度出發(fā),虛擬機(jī)側(cè)有沒有想過的方案檢測(cè)內(nèi)存呢?有的,那就是JVMTI(Java Virtual Machine Tool Interface)即指 Java 虛擬機(jī)工具接口,它是一套由虛擬機(jī)直接提供的 native 接口,我們可以從這里面獲取虛擬機(jī)運(yùn)行時(shí)的大部分信息。
友情提示:本文涉及native c層的代碼,如果讀者不熟悉也沒關(guān)系,已經(jīng)盡量減少相關(guān)的代碼閱讀成本啦!沖就對(duì)啦!JVMTI在debug模式下有很多用處,當(dāng)然release環(huán)境也可以通過hook方式開啟,但是不太建議,雖然jvmti有諸多限制,但是不妨礙我們多了解一個(gè)“黑科技”
JVMTI
JVMTI 簡(jiǎn)介:
JVMTI,即由java虛擬機(jī)提供的面向虛擬機(jī)接口的一套監(jiān)控api,雖然虛擬機(jī)中一直存在,但是在android中是在Android 8.0(API 級(jí)別 26)或更高版本的設(shè)備上才正式支持。jvmti的功能本質(zhì)就是“埋點(diǎn)化”,把jvm的一些事件通過“監(jiān)聽”的方式暴露給外部開發(fā)調(diào)試
jvmti監(jiān)聽的事件包包含了虛擬機(jī)中線程、內(nèi)存、堆、棧、類、方法、變量,事件、定時(shí)器,鎖等創(chuàng)建銷毀相關(guān)事件,本次我們從實(shí)戰(zhàn)的角度出發(fā),看看如何實(shí)現(xiàn)一次內(nèi)存分配的監(jiān)聽。
native層開啟jvmti
前置準(zhǔn)備
使用jvmti之前,我們需要?jiǎng)?chuàng)建一個(gè)native工程,同時(shí)我們需要使用jvmti的api,在native中就是頭文件了,我們需要復(fù)制一份jdk中的名叫jvmti.h的頭文件(在我們安裝的jdk/include目錄下),到我們的項(xiàng)目cpp根目錄即可
此時(shí)我們也自定義一個(gè)memory.cpp作為我們使用jvmti的函數(shù)載體。jvmti.h里面包含了我們所需要的一切函數(shù)定義與常量,當(dāng)然,這個(gè)頭文件并不需要隨著native工程進(jìn)行打包,因?yàn)樵谡嬲褂玫絡(luò)vmti相關(guān)的工具時(shí),是由系統(tǒng)進(jìn)行so依賴查找進(jìn)行定位的,該so位于系統(tǒng)庫(kù)中(libopenjdkjvmtid.so、libopenjdkjvmti.so),所以我們不用關(guān)心具體的實(shí)現(xiàn),接下來我們按照步驟進(jìn)行即可,包括native層與java層
復(fù)寫Agent
作為第一步,我們需要復(fù)寫jvmti.h中的
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
這個(gè)是jvmti中的agent初始化的時(shí)候,由native回調(diào),在這里我們可以拿到JavaVM環(huán)境,同時(shí)可以創(chuàng)建jvmtiEnv對(duì)象,該對(duì)象非常重要,用于native進(jìn)行接下來的各種監(jiān)聽處理
// 全局的jvmti環(huán)境變量 jvmtiEnv *mJvmtiEnv;
extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options, void *reserved) { //準(zhǔn)備JVMTI環(huán)境,初始化mJvmtiEnv vm->GetEnv((void **) &mJvmtiEnv, JVMTI_VERSION_1_2); return JNI_OK; }
開啟jvmtiCapabilities
默認(rèn)時(shí),jvmti中是不提供任何能力給我們使用的,我們可以通過jvmtiEnv,去查詢當(dāng)前虛擬機(jī)實(shí)現(xiàn)的哪幾種jvmti回調(diào)
jvmtiError GetPotentialCapabilities(jvmtiCapabilities* capabilities_ptr) { return functions->GetPotentialCapabilities(this, capabilities_ptr); } jvmtiError AddCapabilities(const jvmtiCapabilities* capabilities_ptr) { return functions->AddCapabilities(this, capabilities_ptr); }
可以看到,我們只需要傳入一個(gè)jvmtiCapabilities對(duì)象指針即可,之后的能力數(shù)據(jù)就會(huì)被填充到該對(duì)象,所以我們接下來在Agent_OnAttach函數(shù)中繼續(xù)補(bǔ)充以下代碼
//初始化工作 extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char *options, void *reserved) { //準(zhǔn)備JVMTI環(huán)境,初始化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; }
設(shè)置jvmtiEventCallbacks
我們已經(jīng)查詢到了jvmti所支持的回調(diào),這個(gè)時(shí)候就到了正式設(shè)置回調(diào)的環(huán)節(jié),jvmti中支持以下幾種回調(diào)類型
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;
我們需要監(jiān)聽的是內(nèi)存分配與銷毀的監(jiān)聽即可,分別是VMObjectAlloc與ObjectFree,在jvmtiEventCallbacks設(shè)定我們想要監(jiān)聽的事件之后,我們可以通過jvmtiEnv->SetEventCallbacks方法設(shè)定即可,所以我們可以繼續(xù)在Agent_OnAttach中補(bǔ)充以下代碼
jvmtiEventCallbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.VMObjectAlloc = &objectAlloc; callbacks.ObjectFree = &objectFree; //設(shè)置回調(diào)函數(shù) mJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
其中objectAlloc是我們自定義的監(jiān)聽處理函數(shù),如果jvm執(zhí)行內(nèi)存分配事件,就會(huì)回調(diào)此函數(shù),該函數(shù)定義是
typedef void (JNICALL *jvmtiEventVMObjectAlloc) (jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jobject object, jclass object_klass, jlong size);
所以我們自定義的回調(diào)函數(shù)也要根據(jù)此定義進(jìn)行編寫。因?yàn)檫@里會(huì)回調(diào)所有java層的對(duì)象創(chuàng)建事件,回調(diào)次數(shù)非常多,在實(shí)際中我們可能并不關(guān)心系統(tǒng)類是如何分配內(nèi)存的,而是關(guān)心我們自己的項(xiàng)目中的類的內(nèi)存情況,所以這里我們做一個(gè)過濾,只有是項(xiàng)目的類我們才進(jìn)行記錄
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); // 這個(gè)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); }
我們可以看到,我們?cè)谥虚g做了一個(gè)jvmti_env->SetTag的操作,這個(gè)是給這個(gè)分配的對(duì)象進(jìn)行了一個(gè)打標(biāo)簽的動(dòng)作(我們需要觀察該對(duì)象是否被銷毀,所以需要一個(gè)唯一標(biāo)識(shí)符),我們會(huì)在釋放的時(shí)候用到。因?yàn)榛卣{(diào)的操作可能會(huì)有很多,我們采用普通的io必定會(huì)導(dǎo)致native層的阻塞,所以這里就要靠我們的mmap登場(chǎng)了,通過mmap我們可以高效的處理頻繁的io,mmap不熟悉的可以看這篇,memoryFile->write是一個(gè)通過mmap的寫文件操作。
objectFree是我們的釋放內(nèi)存的監(jiān)聽,它的函數(shù)定義是
typedef void (JNICALL *jvmtiEventObjectFree) (jvmtiEnv *jvmti_env, jlong tag);
可以看到,我們?cè)卺尫艃?nèi)存的時(shí)候得到的信息非常有限,只有一個(gè)tag,也就是我們?cè)诜峙鋬?nèi)存時(shí)通過SetTag操作所得到的參數(shù),如果有設(shè)置就就會(huì)為具體的tag數(shù)值。我們?cè)谶@個(gè)函數(shù)中的業(yè)務(wù)邏輯就是記錄當(dāng)次的釋放記錄即可
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)); } }
我們?cè)倩氐缴鲜龃a留下的疑問,list是個(gè)什么?其實(shí)就是記錄了我們?cè)赩MObjectAlloc階段所分配的屬于我們自定義的類的tag,因?yàn)镺bjectFree提供給我們的信息非常有限,只有一個(gè)tag,如果不通過這個(gè)list保存分配內(nèi)存時(shí)的tag的話,就會(huì)導(dǎo)致釋放的時(shí)候我們引入過多的不必要的釋放記錄。但是這里也帶來了一個(gè)問題,就是我們需要時(shí)刻同步list的狀態(tài),因?yàn)閖vmti是可以在多線程環(huán)境下回調(diào),如果只是簡(jiǎn)單操作list的話就會(huì)帶來同步問題(這里我們沒有處理,為了demo的簡(jiǎn)單)真實(shí)操作上我們最好加入mutex鎖或者其他機(jī)制保證同步問題。
下面我們?cè)俳o出memoryFile->write的代碼
currentSize 記錄當(dāng)前大小 m_size 以頁(yè)為單位的默認(rèn)大小 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的大小不夠,就需要重新進(jìn)行mmap操作,以頁(yè)為單位 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)); }
開啟監(jiān)聽
到這里,我們還沒有結(jié)束,我們需要真正的開啟監(jiān)聽,前面只是設(shè)置監(jiān)聽的操作,我們可以通過SetEventNotificationMode函數(shù)開啟真正監(jiān)聽/關(guān)閉監(jiān)聽
jvmtiError SetEventNotificationMode(jvmtiEventMode mode, jvmtiEvent event_type, jthread event_thread, ...) { return functions->SetEventNotificationMode(this, mode, event_type, event_thread); }
mode代表當(dāng)前狀態(tài),是個(gè)枚舉,event_type就是我們要開啟監(jiān)聽的類型(這里我們指定為內(nèi)存分配與釋放事件即可),event_thread可以指定某個(gè)線程的內(nèi)存分配事件,null就是全局監(jiān)聽,所以我們的業(yè)務(wù)代碼如下
//開啟監(jiān)聽 mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, nullptr); mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_OBJECT_FREE, nullptr);
java層開啟agent
通過在native層設(shè)置了jvmti的監(jiān)聽與實(shí)現(xiàn),我們還要在java層通過Debug.attachJvmtiAgent(9.0)進(jìn)行開啟,這里有細(xì)微差距
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庫(kù)是否存在,不存在復(fù)制過來 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庫(kù) 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) } } // 設(shè)置mmap的文件path external fun initMemoryCallBack(path: String) }
attachJvmtiAgent方法需要實(shí)現(xiàn)了jvmti 的so庫(kù)的絕對(duì)地址,那么我們?nèi)绾尾檎乙粋€(gè)so庫(kù)的地址呢?其實(shí)就是通過ClassLoader的findLibrary方法,我們可以獲取到so的絕對(duì)地址,不過這個(gè)絕對(duì)地址不能夠直接用,我們看一下源碼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 會(huì)進(jìn)行格式校驗(yàn)Preconditions.checkArgument(!library.contains("=")),恰好我們得到的so的地址是包含=的,所以才需要一個(gè)File的copy操作(拷貝到一個(gè)不包含=的目錄下)
驗(yàn)證分配數(shù)據(jù)
通過上面的jvmti操作,我們已經(jīng)可以將數(shù)據(jù)保存到本地文件了,本地文件的保存可以自己定義,這里我保存在context.filesDir目錄中/log子目錄下,同時(shí)我們生成一個(gè)測(cè)試數(shù)據(jù)
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) }
運(yùn)行后
我們就完成了一個(gè)內(nèi)存的記錄,通過該記錄我們就能夠分析哪些類引起了內(nèi)存問題(即存在分配tag不存在釋放tag)
總結(jié)
到這里,我們終于完成了一個(gè)jvmti的監(jiān)控操作!當(dāng)然,上面的代碼還有很多需要提升的地方,比如多線程引用,比如我們可以同時(shí)開啟MethodEntry的callback記錄一個(gè)方法的開始和結(jié)束,為內(nèi)存泄漏的定位做更加詳細(xì)的分析等等!因?yàn)槠邢蓿@里就當(dāng)作拓展留給讀者們自行實(shí)現(xiàn)啦,以上就是Android性能優(yōu)化之JVMTI與內(nèi)存分配的詳細(xì)內(nèi)容,更多關(guān)于Android性能JVMTI內(nèi)存分配的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Kotlin注解實(shí)現(xiàn)Parcelable序列化流程詳解
有時(shí)我們會(huì)在界面跳轉(zhuǎn)的過程中,做對(duì)象傳值,這時(shí)就需要對(duì)該對(duì)象做序列化處理了。Android中對(duì)對(duì)象的序列化處理有兩種方式,這篇文章主要介紹了Kotlin注解實(shí)現(xiàn)Parcelable序列化2022-12-12Android實(shí)現(xiàn)Tab布局的4種方式(Fragment+TabPageIndicator+ViewPager)
Android現(xiàn)在實(shí)現(xiàn)Tab類型的界面方式越來越多,本文詳細(xì)介紹了Android實(shí)現(xiàn)Tab布局的4種方式,具有一定的參考價(jià)值,有興趣的可以了解一下。2016-11-11Android 擴(kuò)大 View 的點(diǎn)擊區(qū)域的方法
這篇文章主要介紹了Android 擴(kuò)大 View 的點(diǎn)擊區(qū)域的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04鴻蒙開源第三方組件之連續(xù)滾動(dòng)圖像組件功能
這篇文章主要介紹了鴻蒙開源第三方組件之連續(xù)滾動(dòng)圖像組件功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04Kotlin 創(chuàng)建接口或者抽象類的匿名對(duì)象實(shí)例
這篇文章主要介紹了Kotlin 創(chuàng)建接口或者抽象類的匿名對(duì)象實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-03-03android 通過MediaRecorder實(shí)現(xiàn)簡(jiǎn)單的錄音示例
本篇文章中主要介紹了android 通過MediaRecorder實(shí)現(xiàn)簡(jiǎn)單的錄音示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02Android LayoutInflater.inflate()詳解及分析
這篇文章主要介紹了Android LayoutInflater.inflate()詳解及分析的相關(guān)資料,需要的朋友可以參考下2017-01-01