亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Android性能優(yōu)化之JVMTI與內(nèi)存分配

 更新時間:2022年10月09日 17:16:44   作者:Pika  
這篇文章主要為大家介紹了Android性能優(yōu)化之JVMTI與內(nèi)存分配,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

內(nèi)存治理一直是每個開發(fā)者最關(guān)心的問題,我們在日常開發(fā)中會遇到各種各樣的內(nèi)存問題,比如OOM,內(nèi)存泄露,內(nèi)存抖動等等,這些問題都有以下共性:

  • 難發(fā)現(xiàn),內(nèi)存問題一般很難發(fā)現(xiàn),業(yè)務(wù)開發(fā)中關(guān)系系數(shù)更少
  • 治理困難,內(nèi)存問題治理困難,比如oom,往往堆棧只是壓死駱駝的最后一根稻草
  • 易復(fù)發(fā),幾乎沒有一種方案,能夠杜絕內(nèi)存問題,比如內(nèi)存泄露幾乎是100%存在,只是不同項目影響的范圍不同而已

內(nèi)存問題目前經(jīng)過業(yè)內(nèi)多年沉淀以及開發(fā),已經(jīng)有很多方案了,比如檢查內(nèi)存泄露(LeakCanary,MIT,KOOM等)。相關(guān)文章已經(jīng)有很多,所以我們從另一個角度出發(fā),虛擬機側(cè)有沒有想過的方案檢測內(nèi)存呢?有的,那就是JVMTI(Java Virtual Machine Tool Interface)即指 Java 虛擬機工具接口,它是一套由虛擬機直接提供的 native 接口,我們可以從這里面獲取虛擬機運行時的大部分信息。

友情提示:本文涉及native c層的代碼,如果讀者不熟悉也沒關(guān)系,已經(jīng)盡量減少相關(guān)的代碼閱讀成本啦!沖就對啦!JVMTI在debug模式下有很多用處,當(dāng)然release環(huán)境也可以通過hook方式開啟,但是不太建議,雖然jvmti有諸多限制,但是不妨礙我們多了解一個“黑科技”

JVMTI

JVMTI 簡介:

JVMTI,即由java虛擬機提供的面向虛擬機接口的一套監(jiān)控api,雖然虛擬機中一直存在,但是在android中是在Android 8.0(API 級別 26)或更高版本的設(shè)備上才正式支持。jvmti的功能本質(zhì)就是“埋點化”,把jvm的一些事件通過“監(jiān)聽”的方式暴露給外部開發(fā)調(diào)試

jvmti監(jiān)聽的事件包包含了虛擬機中線程、內(nèi)存、堆、棧、類、方法、變量,事件、定時器,鎖等創(chuàng)建銷毀相關(guān)事件,本次我們從實戰(zhàn)的角度出發(fā),看看如何實現(xiàn)一次內(nèi)存分配的監(jiān)聽。

native層開啟jvmti

前置準(zhǔn)備

使用jvmti之前,我們需要創(chuàng)建一個native工程,同時我們需要使用jvmti的api,在native中就是頭文件了,我們需要復(fù)制一份jdk中的名叫jvmti.h的頭文件(在我們安裝的jdk/include目錄下),到我們的項目cpp根目錄即可

此時我們也自定義一個memory.cpp作為我們使用jvmti的函數(shù)載體。jvmti.h里面包含了我們所需要的一切函數(shù)定義與常量,當(dāng)然,這個頭文件并不需要隨著native工程進(jìn)行打包,因為在真正使用到j(luò)vmti相關(guān)的工具時,是由系統(tǒng)進(jìn)行so依賴查找進(jìn)行定位的,該so位于系統(tǒng)庫中(libopenjdkjvmtid.so、libopenjdkjvmti.so),所以我們不用關(guān)心具體的實現(xiàn),接下來我們按照步驟進(jìn)行即可,包括native層與java層

復(fù)寫Agent

作為第一步,我們需要復(fù)寫jvmti.h中的

JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);

這個是jvmti中的agent初始化的時候,由native回調(diào),在這里我們可以拿到JavaVM環(huán)境,同時可以創(chuàng)建jvmtiEnv對象,該對象非常重要,用于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)時,jvmti中是不提供任何能力給我們使用的,我們可以通過jvmtiEnv,去查詢當(dāng)前虛擬機實現(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);
}

可以看到,我們只需要傳入一個jvmtiCapabilities對象指針即可,之后的能力數(shù)據(jù)就會被填充到該對象,所以我們接下來在Agent_OnAttach函數(shù)中繼續(xù)補充以下代碼

//初始化工作
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),這個時候就到了正式設(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中補充以下代碼

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)存分配事件,就會回調(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)行編寫。因為這里會回調(diào)所有java層的對象創(chuàng)建事件,回調(diào)次數(shù)非常多,在實際中我們可能并不關(guān)心系統(tǒng)類是如何分配內(nèi)存的,而是關(guān)心我們自己的項目中的類的內(nèi)存情況,所以這里我們做一個過濾,只有是項目的類我們才進(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( &currentEnv, 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的操作,這個是給這個分配的對象進(jìn)行了一個打標(biāo)簽的動作(我們需要觀察該對象是否被銷毀,所以需要一個唯一標(biāo)識符),我們會在釋放的時候用到。因為回調(diào)的操作可能會有很多,我們采用普通的io必定會導(dǎo)致native層的阻塞,所以這里就要靠我們的mmap登場了,通過mmap我們可以高效的處理頻繁的io,mmap不熟悉的可以看這篇,memoryFile->write是一個通過mmap的寫文件操作。

objectFree是我們的釋放內(nèi)存的監(jiān)聽,它的函數(shù)定義是

typedef void (JNICALL *jvmtiEventObjectFree)
    (jvmtiEnv *jvmti_env,
     jlong tag);

可以看到,我們在釋放內(nèi)存的時候得到的信息非常有限,只有一個tag,也就是我們在分配內(nèi)存時通過SetTag操作所得到的參數(shù),如果有設(shè)置就就會為具體的tag數(shù)值。我們在這個函數(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));
    }
}

我們再回到上述代碼留下的疑問,list是個什么?其實就是記錄了我們在VMObjectAlloc階段所分配的屬于我們自定義的類的tag,因為ObjectFree提供給我們的信息非常有限,只有一個tag,如果不通過這個list保存分配內(nèi)存時的tag的話,就會導(dǎo)致釋放的時候我們引入過多的不必要的釋放記錄。但是這里也帶來了一個問題,就是我們需要時刻同步list的狀態(tài),因為jvmti是可以在多線程環(huán)境下回調(diào),如果只是簡單操作list的話就會帶來同步問題(這里我們沒有處理,為了demo的簡單)真實操作上我們最好加入mutex鎖或者其他機制保證同步問題。

下面我們再給出memoryFile->write的代碼

currentSize 記錄當(dāng)前大小 m_size 以頁為單位的默認(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操作,以頁為單位
    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),是個枚舉,event_type就是我們要開啟監(jiān)聽的類型(這里我們指定為內(nèi)存分配與釋放事件即可),event_thread可以指定某個線程的內(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)聽與實現(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庫是否存在,不存在復(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庫
            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方法需要實現(xiàn)了jvmti 的so庫的絕對地址,那么我們?nèi)绾尾檎乙粋€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 會進(jìn)行格式校驗Preconditions.checkArgument(!library.contains("=")),恰好我們得到的so的地址是包含=的,所以才需要一個File的copy操作(拷貝到一個不包含=的目錄下)

驗證分配數(shù)據(jù)

通過上面的jvmti操作,我們已經(jīng)可以將數(shù)據(jù)保存到本地文件了,本地文件的保存可以自己定義,這里我保存在context.filesDir目錄中/log子目錄下,同時我們生成一個測試數(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)
}

運行后

我們就完成了一個內(nèi)存的記錄,通過該記錄我們就能夠分析哪些類引起了內(nèi)存問題(即存在分配tag不存在釋放tag)

總結(jié)

到這里,我們終于完成了一個jvmti的監(jiān)控操作!當(dāng)然,上面的代碼還有很多需要提升的地方,比如多線程引用,比如我們可以同時開啟MethodEntry的callback記錄一個方法的開始和結(jié)束,為內(nèi)存泄漏的定位做更加詳細(xì)的分析等等!因為篇幅有限,這里就當(dāng)作拓展留給讀者們自行實現(xiàn)啦,以上就是Android性能優(yōu)化之JVMTI與內(nèi)存分配的詳細(xì)內(nèi)容,更多關(guān)于Android性能JVMTI內(nèi)存分配的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論