<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          核桃干貨 | Android常見內(nèi)存泄漏與優(yōu)化

          共 17537字,需瀏覽 36分鐘

           ·

          2021-03-14 11:21



          Android虛擬機(jī):Dalvik和ART


          Dalvik是Google特別設(shè)計(jì)專門用于Android平臺(tái)的虛擬機(jī),它位于Android系統(tǒng)架構(gòu)的Android的運(yùn)行環(huán)境(Android Runtime)中,是Android移動(dòng)設(shè)備平臺(tái)的核心組成部分之一。

          類似于傳統(tǒng)的JVM,Dalvik虛擬機(jī)是在Android操作系統(tǒng)上虛擬出來的一個(gè)“設(shè)備”,用來運(yùn)行Android應(yīng)用程序(APP),主要負(fù)責(zé)堆棧管理、線程管理、安全及異常管理、垃圾回收、對(duì)象的生命周期管理等。在Android系統(tǒng)中,每一個(gè)APP對(duì)應(yīng)著一個(gè)Dalvik虛擬機(jī)實(shí)例。

          Dalvik虛擬機(jī)支持.dex(即"Dalvik Executable")格式的Java應(yīng)用程序的運(yùn)行,.dex是專為Dalvik設(shè)計(jì)的一種壓縮格式,它是在.class字節(jié)碼文件的基礎(chǔ)上經(jīng)過DEX工具壓縮、優(yōu)化后得到的,適用于內(nèi)存和處理器速度有限的系統(tǒng)。

          在Android 4.4以前的系統(tǒng)中,Android系統(tǒng)均采用Dalvik作為運(yùn)行Android應(yīng)用的虛擬機(jī),但隨著Dalvik的不足逐漸暴露,到Android 5.0以后的系統(tǒng)使用ART虛擬機(jī)完全取代了Dalvik虛擬機(jī)。

          ART虛擬機(jī)在性能上做了很多優(yōu)化,比如采用預(yù)編譯(AOT,Ahead Of Time compilation)取代JIT編譯器、支持64位CPU、改進(jìn)垃圾回收機(jī)制等等,從而使得Android系統(tǒng)運(yùn)行更為流暢。

          下圖展示了Android系統(tǒng)架構(gòu)和DVM架構(gòu):

          ?

          Android虛擬機(jī)的使用,使得Android應(yīng)用和Linux內(nèi)核分離,從而使得Android系統(tǒng)更加穩(wěn)定可靠,也就是說,即便是某個(gè)Android程序被嵌入了惡意代碼,也不會(huì)直接影響系統(tǒng)的正常運(yùn)行。

          接下來,我們就從分析JVM、Dalvik、ART三者之間的關(guān)系,來進(jìn)一步了解它們。

          1.1 JVM與Dalvik區(qū)別

          在Android 4.4以前,Android中的所有Java程序都是運(yùn)行在Dalvik虛擬機(jī)上的,每個(gè)Android應(yīng)用進(jìn)程對(duì)應(yīng)著一個(gè)獨(dú)立的Dalvik虛擬機(jī)實(shí)例并在其解釋下執(zhí)行。

          雖然Dalvik虛擬機(jī)也是用來運(yùn)行Java程序,但是它并沒有遵守Java虛擬機(jī)規(guī)范來實(shí)現(xiàn),是Google為Android平臺(tái)特殊設(shè)計(jì)且運(yùn)行在Android 運(yùn)行時(shí)庫的虛擬機(jī),因此Dalvik虛擬機(jī)并不是一個(gè)Java虛擬機(jī)。它們的主要區(qū)別如下:

          (1) 基于的架構(gòu)不同

          JVM基于棧架構(gòu),Dalvik虛擬機(jī)基于寄存器架構(gòu)。JVM基于棧則意味著需要去棧中讀寫數(shù)據(jù),所需更多的指令會(huì)更多(主要是load/store指令),這樣會(huì)導(dǎo)致速度變慢,對(duì)于性能有限的移動(dòng)設(shè)備,顯然是不合適的;

          Dalvik虛擬機(jī)基于寄存器實(shí)現(xiàn),則意味著它的指令會(huì)更加緊湊、簡潔,因?yàn)樘摂M機(jī)在復(fù)制數(shù)據(jù)時(shí)不需要使用大量的出入棧指令,但由于需要指定源地址和目標(biāo)地址,所以基于寄存器的指令會(huì)比基于棧的指令要大,當(dāng)然,由于指令數(shù)量的減少,總的代碼數(shù)不會(huì)增加多少。

          下圖的一段Java程序代碼,展示了在JVM和Dalvik虛擬機(jī)中字節(jié)碼的表現(xiàn)形式:

          Java字節(jié)碼以單字節(jié)(1 byte)為單元,JVM使用的指令只占1個(gè)單元;Dalvik字節(jié)碼以雙字節(jié)(2 byte)為單元,Dalvik虛擬機(jī)使用的指令占1個(gè)單元或2個(gè)單元。因此,在上面的代碼中JVM字節(jié)碼占11個(gè)單元=11字節(jié),Dalvik字節(jié)碼占6個(gè)單元=12字節(jié)(其中,mul-int/lit8指令占2單元)。

          (2) 執(zhí)行的字節(jié)碼文件不同

          ?

          JVM運(yùn)行的.class文件,Dalvik運(yùn)行的是.dex(即Dalvik Executable)文件。在Java程序中,Java類會(huì)編譯成一個(gè)或多個(gè).class文件,然后打包到.jar文件中,.jar文件中的每個(gè).class文件里面包含了該類的常量池、類信息、屬性等。


          當(dāng)JVM加載該.jar文件時(shí),會(huì)加載里面的所有的.class文件,JVM的這種加載方式很慢,對(duì)于內(nèi)存有限的移動(dòng)設(shè)備并不合適;.dex文件是在.class文件的基礎(chǔ)上,經(jīng)過DEX工具壓縮和優(yōu)化后形成的,通常每一個(gè).apk文件中只包含了一個(gè).dex,這個(gè).dex文件將所有的.class里面所包含的信息全部整合在一起了,這樣做的好處就是減少了整體的文件尺寸(去除了.class文件中相同的冗余信息),同時(shí)減少了I/O操作,加快了類的查找速度。下圖展示了.jar和.dex的對(duì)比差異:

          (3) 在內(nèi)存中的表現(xiàn)形式差異


          Dalvik經(jīng)過優(yōu)化,允許在有限的內(nèi)存中同時(shí)運(yùn)行多個(gè)進(jìn)程,或說同時(shí)運(yùn)行多個(gè)Dalvik虛擬機(jī)的實(shí)例。


          在Android中每一個(gè)應(yīng)用都運(yùn)行在一個(gè)Dalvik虛擬機(jī)實(shí)例中,每一個(gè)Dalvik虛擬機(jī)實(shí)例都運(yùn)行在一個(gè)獨(dú)立的進(jìn)程空間中,因此都對(duì)應(yīng)著一個(gè)獨(dú)立的進(jìn)程,獨(dú)立的進(jìn)程可以防止在虛擬機(jī)崩潰時(shí)所有程序都被關(guān)閉。而對(duì)于JVM來說,在其宿主OS的內(nèi)存中只運(yùn)行著一個(gè)JVM的實(shí)例,這個(gè)JVM實(shí)例中可以運(yùn)行多個(gè)Java應(yīng)用程序(進(jìn)程),但是一旦JVM異常崩潰,就會(huì)導(dǎo)致運(yùn)行在其中的所有程序被關(guān)閉。


          (4) Dalvik擁有Zygote進(jìn)程與共享機(jī)制

          ?

          在Android系統(tǒng)中有個(gè)一特殊的虛擬機(jī)進(jìn)程--Zygote,它是虛擬機(jī)實(shí)例的孵化器。它在Android系統(tǒng)啟動(dòng)的時(shí)候就會(huì)產(chǎn)生,完成虛擬機(jī)的初始化、庫的加載、預(yù)制類庫和初始化操作。


          如果系統(tǒng)需要一個(gè)新的虛擬機(jī)實(shí)例,他會(huì)迅速復(fù)制自身,以最快的速度提供給系統(tǒng)。對(duì)于一些只讀的系統(tǒng)庫,所有的虛擬機(jī)實(shí)例都和Zygote共享一塊區(qū)域。Dalvik虛擬機(jī)擁有預(yù)加載-共享的機(jī)制,使得不同的應(yīng)用之間在運(yùn)行時(shí)可以共享相同的類,因此擁有更高的效率。而JVM則不存在這個(gè)共享機(jī)制,不同的程序被打包后都是彼此獨(dú)立的,即便它們?cè)诎锸褂昧讼嗤念?,運(yùn)行時(shí)的都是單獨(dú)加載和運(yùn)行,無法進(jìn)行共享。

          1.2 Dalvik與ART區(qū)別


          ART虛擬機(jī)被引入于Android 4.4,用來替換Dalvik虛擬機(jī),以緩解Dalvik虛擬機(jī)的運(yùn)行機(jī)制導(dǎo)致Android應(yīng)用運(yùn)行變慢的問題。在Android 4.4中,可以選擇使用Dalvik還是ART,而從Android 5.0開始,Dalvik被完全刪除,Android系統(tǒng)默認(rèn)采用ART。Dalvik與ART的主要區(qū)別如下:


          (1) ART運(yùn)行機(jī)制優(yōu)于Dalvik

          ?

          對(duì)于運(yùn)行在Dalvik虛擬機(jī)實(shí)例中的應(yīng)用程序而言,在每一次重新運(yùn)行的時(shí)候,都需要將字節(jié)碼通過JIT(Just-In-Time)編譯器編譯成機(jī)器碼,這會(huì)使用應(yīng)用程序的運(yùn)行效率降低,雖然Dalvik虛擬機(jī)已經(jīng)被做過很多優(yōu)化(.dex文件->.odex文件),但由于這種先翻譯再執(zhí)行的機(jī)制仍然無法有效解決Dalvik拖慢Android應(yīng)用運(yùn)行的事實(shí)。


          而在ART中,系統(tǒng)在安裝應(yīng)用程序時(shí)會(huì)進(jìn)行一次AOT(Ahead Of Time compilication,預(yù)編譯),即將字節(jié)碼預(yù)先編譯成機(jī)器碼并存儲(chǔ)在本地,這樣應(yīng)用程序每次運(yùn)行時(shí)就不需要執(zhí)行編譯了,運(yùn)行效率會(huì)大大提高。

          (2) 支持的CPU架構(gòu)不同

          ?

          Dalvik是為32位CPU設(shè)計(jì)的,而ART支持64位并兼容32位的CPU。


          (3) 運(yùn)行時(shí)堆劃分不同

          ?

          Dalvik虛擬機(jī)的運(yùn)行時(shí)堆使用標(biāo)記--清除(Mark--Sweep)算法進(jìn)行GC,它由兩個(gè)Space以及多個(gè)輔助數(shù)據(jù)結(jié)構(gòu)組成,兩個(gè)Space分別是Zygote Space(Zygote Heap)和Allocation Space(Active Heap)。


          Zygote Space用來管理Zygote進(jìn)程在啟動(dòng)過程中預(yù)加載和創(chuàng)建的各種對(duì)象,Zygote Space中不會(huì)觸發(fā)GC,應(yīng)用進(jìn)程和Zygote進(jìn)程之間會(huì)共享Zygote Space。


          Zygote進(jìn)程在fork第一個(gè)子進(jìn)程之前,會(huì)把Zygote Space分為兩個(gè)部分,原來被Zygote進(jìn)程使用的部分仍然叫Zygote Space,而剩余未被使用的部分被稱為Allocation Space,以后fork的子進(jìn)程相關(guān)的所有的對(duì)象都會(huì)在Allocation Space上進(jìn)行分配和釋放。


          需要注意的是,Allocation Space不是進(jìn)程共享的,在每個(gè)進(jìn)程中都獨(dú)立擁有一份。下圖展示了Dalvik虛擬機(jī)的運(yùn)行時(shí)堆結(jié)構(gòu):


          與Dalvik的GC不同,ART采用了多種垃圾收集方案,每個(gè)方案會(huì)運(yùn)行不同的垃圾收集器,默認(rèn)是采用了CMS(Concurrent Mark-Sweep)方案,該方案主要使用了sticky-CMS和patial-CMS。

          根據(jù)不同的CMS方案,ART的運(yùn)行時(shí)堆的空間劃分也會(huì)不同,默認(rèn)是由4個(gè)Space和多個(gè)輔助數(shù)據(jù)結(jié)構(gòu)組成,4個(gè)Space分別是Zygote Space、Allocation Space、Image Space以及Large Object Space,其中,Zygote Space和Allocation Space和Dalvik的一樣,Image Space用來存放一些預(yù)加載類,Large Object Space用來分配一些大對(duì)象(默認(rèn)大小為12Kb)。

          需要注意的是,Zygote Space和Image Space是進(jìn)程間共享的。下圖展示了ART采用標(biāo)記–清除算法的堆劃分:
          ART虛擬機(jī)的不足:
          • 安裝時(shí)間變長。應(yīng)用在安裝的時(shí)候需要預(yù)編譯,從而增大了安裝時(shí)間。
          • 存儲(chǔ)空間變大。ART引入AOT技術(shù)后,需要更多的空間存儲(chǔ)預(yù)編譯后的機(jī)器碼。

          因此,從某些程度上來說,ART虛擬機(jī)是利用了“空間換時(shí)間”,來提高Android應(yīng)用的運(yùn)行速度。

          為了緩解上述的不足,Android7.0(N)版本中的ART加入了即時(shí)編譯器JIT,作為AOT的一個(gè)補(bǔ)充,在應(yīng)用程序安裝時(shí)并不會(huì)將字節(jié)碼全部編譯成機(jī)器碼,而是在運(yùn)行中將熱點(diǎn)代碼編譯成機(jī)器碼,具體來說,就是當(dāng)我們第一次運(yùn)行應(yīng)用相關(guān)程序后,JIT提供了一套追蹤機(jī)制來決定哪一部分代碼需要在手機(jī)處于idle狀態(tài)和充電的時(shí)候來編譯,并將編譯得到的機(jī)器碼存儲(chǔ)到本地,這個(gè)追蹤技術(shù)被稱為Profile Guided Compilcation。

          1.3 Dalvik/ART的啟動(dòng)流程
          ?
          從剖析Android系統(tǒng)的啟動(dòng)過程一文可知,init進(jìn)程(pid=1)是Linux系統(tǒng)的用戶進(jìn)程,是所有用戶進(jìn)程的父進(jìn)程。

          當(dāng)Android系統(tǒng)在啟動(dòng)init進(jìn)程后,該進(jìn)程會(huì)孵化出一堆用戶守護(hù)進(jìn)程、ServiceManager服務(wù)以及Zygote進(jìn)程等等,其中,Zygote進(jìn)程是Android系統(tǒng)啟動(dòng)的第一個(gè)Java進(jìn)程,或者說是虛擬機(jī)進(jìn)程,因?yàn)樗钟蠨alvik或ART的實(shí)例。

          Zygote進(jìn)程是所有Java進(jìn)程的父進(jìn)程,每當(dāng)系統(tǒng)需要?jiǎng)?chuàng)建一個(gè)應(yīng)用程序時(shí),Zygote進(jìn)程就會(huì)fork自身,并快速地創(chuàng)建和初始化一個(gè)虛擬機(jī)實(shí)例,用于應(yīng)用程序的運(yùn)行。

          下面我們就從Android9.0源碼的角度,來分析Zygote進(jìn)程中的Dalvik或ART虛擬機(jī)實(shí)例是如何創(chuàng)建的。
          ?
          首先,根據(jù)init.rc的啟動(dòng)服務(wù)的命令,將運(yùn)行/system/bin/app_process可執(zhí)行程序來啟動(dòng)zygote進(jìn)程,該可執(zhí)行程序?qū)?yīng)的源文件為../base/cmds/app_process/app_main.cpp,也就是說,當(dāng)從init進(jìn)程發(fā)起啟動(dòng)Zygote進(jìn)程后,會(huì)調(diào)用app_main.cpp的main函數(shù)進(jìn)入Zygote進(jìn)程的啟動(dòng)流程。app_main.cpp的main函數(shù)源碼如下:

          //app_main.cpp$main函數(shù)int main(int argc, char* const argv[]){    ...    // (1) 創(chuàng)建AppRuntime對(duì)象    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
          ... // (2) 解析執(zhí)行init.rc的啟動(dòng)服務(wù)的命令傳入的參數(shù) // 解析后:zygote = true // startSystemServer = true // niceName = zygote (當(dāng)前進(jìn)程名稱) bool zygote = false; bool startSystemServer = false; bool application = false; String8 niceName; String8 className; while (i < argc) { const char* arg = argv[i++]; if (strcmp(arg, "--zygote") == 0) { zygote = true; niceName = ZYGOTE_NICE_NAME; } else if (strcmp(arg, "--start-system-server") == 0) { startSystemServer = true; } else if (strcmp(arg, "--application") == 0) { application = true; } else if (strncmp(arg, "--nice-name=", 12) == 0) { niceName.setTo(arg + 12); } else if (strncmp(arg, "--", 2) != 0) { className.setTo(arg); break; } else { --i; break; } } ... // (3) 設(shè)置進(jìn)程名為Zygote,執(zhí)行ZygoteInit類 // Zygote = true if (!niceName.isEmpty()) { runtime.setArgv0(niceName.string()); set_process_name(niceName.string()); } if (zygote) { runtime.start("com.android.internal.os.ZygoteInit", args, zygote); } else if (className) { runtime.start("com.android.internal.os.RuntimeInit", args, zygote); } else { fprintf(stderr, "Error: no class name or --zygote supplied.\n"); app_usage(); LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied."); return 10; }}
          ?
          從app_main.cpp$main函數(shù)源碼可知,為了啟動(dòng)Zygote進(jìn)程,該函數(shù)主要做個(gè)如下三個(gè)方面的工作,即:

          • 創(chuàng)建AppRuntime實(shí)例。AppRuntime是在app_process.cpp中定義的類,繼承于系統(tǒng)的AndroidRuntime,主要用于創(chuàng)建和初始化虛擬機(jī)。AppRuntime類繼承關(guān)系如下:

          class AppRuntime : public AndroidRuntime{};


          • 解析執(zhí)行init.rc的啟動(dòng)服務(wù)的命令傳入的參數(shù)。/init.zygote64_32.rc文件中啟動(dòng)Zygote的內(nèi)容如下,在<Android源代碼目錄>/system/core/rootdir/ 目錄下可以看到init.zygote32.rc、init.zygote32_64.rc、init.zygote64.rc、init.zygote64_32.rc等文件,這是因?yàn)锳ndroid5.0開始支持64位的編譯,所以Zygote進(jìn)程本身也有32位和64位版本。啟動(dòng)Zygote進(jìn)程命令如下:
            /tasks
            service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote    class main    priority -20    socket zygote stream 660 root system    onrestart write /sys/android_power/request_state wake    onrestart write /sys/power/state on    onrestart restart audioserver    onrestart restart cameraserver    onrestart restart media    onrestart restart netd    writepid /dev/cpuset/foreground/tasks /dev/stune/foreground

          • 執(zhí)行ZygoteInit類。由前面 解析命令傳入的參數(shù)可知,zygote=true說明當(dāng)前程序運(yùn)行的進(jìn)程是Zygote進(jìn)程,將調(diào)用AppRuntime的start函數(shù)執(zhí)行ZygoteInit類,從類名可以看出執(zhí)行該類將進(jìn)入Zygote的初始化流程。

          runtime.start("com.android.internal.os.ZygoteInit", argszygote);

          接著,我們?cè)敿?xì)分析下AppRuntime的start函數(shù)執(zhí)行流程。由于AppRuntime繼承于AndroidRuntime,因此start函數(shù)具體在AndroidRuntime中實(shí)現(xiàn)。

          該函數(shù)主要完成三個(gè)方面的工作:(a) 初始化JNI環(huán)境,啟動(dòng)虛擬機(jī);(b) 為虛擬機(jī)注冊(cè)JNI方法;(c)從傳入的com.android.internal.os.ZygoteInit 類中找到main函數(shù),即調(diào)用ZygoteInit.java類中的main方法。AndroidRuntime$start源碼如下:


          void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote){    ...    // (1) 初始化JNI環(huán)境、啟動(dòng)虛擬機(jī)    JniInvocation jni_invocation;    jni_invocation.Init(NULL);    JNIEnv* env;    if (startVm(&mJavaVM, &env, zygote) != 0) {        return;    }    onVmCreated(env);
          // (2) 為虛擬機(jī)注冊(cè)JNI方法 if (startReg(env) < 0) { ALOGE("Unable to register all android natives\n"); return; }
          ... // (3) 從傳入的com.android.internal.os.ZygoteInit 類中找到main函數(shù),即調(diào)用 // ZygoteInit.java類中的main方法。AndroidRuntime及之前的方法都是native的方法,而此刻 // 調(diào)用的ZygoteInit.main方法是java的方法,到這里我們就進(jìn)入了java的世界 char* slashClassName = toSlashClassName(className); jclass startClass = env->FindClass(slashClassName); if (startClass == NULL) { ALOGE("JavaVM unable to locate class '%s'\n", slashClassName); /* keep going */ } else { jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V"); if (startMeth == NULL) { ALOGE("JavaVM unable to find main() in '%s'\n", className); /* keep going */ } else { env->CallStaticVoidMethod(startClass, startMeth, strArray); #if 0 if (env->ExceptionCheck()) threadExitUncaughtException(env); #endif } } ...}

          至此,隨著AndroidRuntime$startVm函數(shù)被調(diào)用,Init進(jìn)程是如何啟動(dòng)Zygote進(jìn)程和在Zygote進(jìn)程中創(chuàng)建虛擬機(jī)的實(shí)例的這個(gè)過程我們就分析完畢了,也驗(yàn)證了Zygote進(jìn)程在被創(chuàng)建啟動(dòng)后,確實(shí)已經(jīng)持有了虛擬機(jī)的實(shí)例,以至于Zygote進(jìn)程fork自身創(chuàng)建應(yīng)用程序時(shí),應(yīng)用程序也得到了虛擬機(jī)的實(shí)例,這樣就不需要每次啟動(dòng)應(yīng)用程序進(jìn)程都要?jiǎng)?chuàng)建虛擬機(jī)實(shí)例,從而加快了應(yīng)用程序進(jìn)程的啟動(dòng)速度。

          至于被創(chuàng)建的是Dalvik還是ART實(shí)例,我們可以看注釋(1)處調(diào)用了jni_invocation的Init()函數(shù),該函數(shù)源碼如下,位于源碼根目錄下libnativehelper/Jnilnvocation.cpp源文件中。該函數(shù)源碼如下:

          #ifdef __ANDROID__#include <sys/system_properties.h>#endif
          // JniInvocation::Initbool JniInvocation::Init(const char* library) { // Android平臺(tái)標(biāo)志 #ifdef __ANDROID__ char buffer[PROP_VALUE_MAX]; #else char* buffer = NULL; #endif // 獲取“l(fā)ibart.so”或“l(fā)ibdvm.so” library = GetLibrary(library, buffer); const int kDlopenFlags = RTLD_NOW | RTLD_NODELETE; // 加載“l(fā)ibart.so”或“l(fā)ibdvm.so” handle_ = dlopen(library, kDlopenFlags); if (handle_ == NULL) { if (strcmp(library, kLibraryFallback) == 0) { return false; } library = kLibraryFallback; handle_ = dlopen(library, kDlopenFlags); if (handle_ == NULL) { ALOGE("Failed to dlopen %s: %s", library, dlerror()); return false; } } ... return true;}


          從JniInvocation::Init函數(shù)源碼可知,它首先會(huì)調(diào)用JniInvocation::GetLibrary函數(shù)來獲取要指定的虛擬機(jī)庫名稱–“l(fā)ibart.so”或“l(fā)ibdvm.so”,然后調(diào)用JniInvocation::dlopen函數(shù)加載這個(gè)虛擬機(jī)庫。


          通過查閱JniInvocation::GetLibrary函數(shù)源碼可知,如果當(dāng)前不是Debug模式構(gòu)建的,是不允許動(dòng)態(tài)更改虛擬機(jī)動(dòng)態(tài)庫,即默認(rèn)為"libart.so";如果當(dāng)前是Debug模式構(gòu)建且傳入的buffer不為NULL時(shí),就需要通過讀取"persist.sys.dalvik.vm.lib.2"這個(gè)系統(tǒng)屬性來設(shè)置返回的library。JniInvocation::GetLibrary函數(shù)源碼如下:


          static const char* kLibraryFallback = "libart.so";const char* JniInvocation::GetLibrary(const char* library, char* buffer) {  return GetLibrary(library, buffer, &IsDebuggable, &GetLibrarySystemProperty);}
          const char* JniInvocation::GetLibrary(const char* library, char* buffer, bool (*is_debuggable)(), int (*get_library_system_property)(char* buffer)) {#ifdef __ANDROID__ const char* default_library; // 如果不是debug構(gòu)建,不允許更改虛擬機(jī)動(dòng)態(tài)庫 // library = default_library = kLibraryFallback = "libart.so" if (!is_debuggable()) { library = kLibraryFallback; default_library = kLibraryFallback; } else { // 如果是debug構(gòu)建,需要判斷傳入的buffer參數(shù)是否為空 // 如果不為空,default_library賦值為buffer if (buffer != NULL) { if (get_library_system_property(buffer) > 0) { default_library = buffer; } else { default_library = kLibraryFallback; } } else { default_library = kLibraryFallback; } }#else UNUSED(buffer); UNUSED(is_debuggable); UNUSED(get_library_system_property); const char* default_library = kLibraryFallback;#endif if (library == NULL) { library = default_library; }
          return library;}
          // "persist.sys.dalvik.vm.lib.2"是系統(tǒng)屬性// 它的取值可以為libdvm.so或libart.soint GetLibrarySystemProperty(char* buffer) {#ifdef __ANDROID__ return __system_property_get("persist.sys.dalvik.vm.lib.2", buffer);#else UNUSED(buffer); return 0;#endif}


           常見內(nèi)存分析工具


           2.1 Android Profiler
          ?
          Android Profiler引入于Android Studio 3.0,是用來替換之前的Android Monitor,主要用來觀察內(nèi)存(Memory)、網(wǎng)絡(luò)(Network)、CPU使用狀態(tài)的實(shí)時(shí)變化。

          這里我們主要介紹Android Profiler中的Memory Profiler組件,它對(duì)應(yīng)于Android Monitor的Memory Monitor,通過Memory Profiler可以實(shí)時(shí)查看/捕獲存儲(chǔ)內(nèi)存的使用狀態(tài)、強(qiáng)制GC以及跟蹤內(nèi)存分配情況,以便于快速地識(shí)別可能會(huì)導(dǎo)致應(yīng)用卡頓、凍結(jié)甚至崩潰的內(nèi)存泄漏和內(nèi)存抖動(dòng)。

          我們可以通過依次點(diǎn)擊AS控制面板的View->Tool Windows->Profiler或者點(diǎn)擊左下角的圖標(biāo),進(jìn)入Memory Profiler監(jiān)控面板。

          • 標(biāo)注(1~6)說明:
            1:用于強(qiáng)制執(zhí)行垃圾回收事件的按鈕;
            2:用于捕獲堆轉(zhuǎn)儲(chǔ)的按鈕,即Dump the Java heap;
            3:用于放大、縮小、復(fù)位時(shí)間軸的按鈕;
            4 :用于實(shí)時(shí)播放內(nèi)存分配情況的按鈕;
            5:發(fā)生一些事件的記錄(如Activity的跳轉(zhuǎn),事件的輸入,屏幕的旋轉(zhuǎn));
            6:內(nèi)存使用量事件軸,它包括以下內(nèi)容:

            • 一個(gè)堆疊圖表。顯示每個(gè)內(nèi)存類別當(dāng)前使用多少內(nèi)存,如左側(cè)的y軸和頂部的彩色健所示。
            • Java:從Java或Kotlin代碼分配的對(duì)象的內(nèi)存(重點(diǎn)關(guān)注);
            • Native:從C或C++代碼分配的對(duì)象的內(nèi)存(重點(diǎn)關(guān)注);
            • Graphics:圖像緩存等,包括GL surfaces, GL textures等;
            • Stack:棧內(nèi)存(包括java和c/c++);
            • Code:用于處理代碼和資源(如 dex 字節(jié)碼.so 庫和字體)分配的內(nèi)存;
            • Other:系統(tǒng)都不知道是什么類型的內(nèi)存,放在這里;
            • Allocated:從Java或Kotlin代碼分配的對(duì)象數(shù)。
            • 一個(gè)堆疊圖表。顯示每個(gè)內(nèi)存類別當(dāng)前使用多少內(nèi)存,如左側(cè)的y軸和頂部的彩色健所示。

          • 一條虛線。虛線表示分配的對(duì)象數(shù)量,如右側(cè)的y軸所示(5000/15000)。


          • 每個(gè)垃圾回收時(shí)間的圖標(biāo)。


          2.1.1 Allocation Tracker

          Allocation Tracker,即跟蹤一段時(shí)間內(nèi)存分配情況,Memory Profiler能夠顯示內(nèi)存中的每個(gè)Java對(duì)象和JNI引用時(shí)如何分配的。我們需要關(guān)注如下信息:

          • 分配了哪些類型的對(duì)象,分配了多大的空間;
          • 對(duì)象分配的棧調(diào)用,是在哪個(gè)線程中調(diào)用的;
          • 對(duì)象的釋放時(shí)間(只針對(duì)8.0+);
          ?
          如果是Android 8.0以上的設(shè)備,支持隨時(shí)查看對(duì)象的分配情況,具體的步驟如下:在時(shí)間軸上拖動(dòng)以選擇要查看的哪個(gè)區(qū)域(時(shí)間段)的內(nèi)存分配情況,如下圖所示:
          接下來,我們就以上一篇文章中所提及的單例模式引起的內(nèi)存泄漏為例,來檢查內(nèi)存分配的記錄,排查可能存在內(nèi)存泄漏的對(duì)象。具體的步驟如下:

          (1) 瀏覽列表以查找堆計(jì)數(shù)異常大且可能存在泄漏的對(duì)象,即大對(duì)象。為了幫助查找已知類,可以點(diǎn)擊下圖中黃色方框的選項(xiàng)選擇使用Arrange by class或Arrange by Package按類名或者包名進(jìn)行分組,然后再紅色方框中的第一個(gè)選項(xiàng)就會(huì)列出Class Name或Package Name,我們可以直接去查找目標(biāo)類,也可以點(diǎn)擊下圖中的Filter 圖標(biāo) 快速查找某個(gè)類,比如SingleInstanceActivity,當(dāng)然我們還可以使用正則表達(dá)式Regex和大小寫匹配Match Case。紅色方框中其他選項(xiàng)意義:

          • Allocations:堆中動(dòng)態(tài)分配對(duì)象個(gè)數(shù);
          • Deallocations:解除分配的對(duì)象個(gè)數(shù);
          • Total Counts:目前存在的對(duì)象總數(shù);
          • Shallow Size:堆中所有對(duì)象的總大小(以字節(jié)為單位),不包含其引用的對(duì)象;
          ?
          (2) 當(dāng)點(diǎn)擊SingleInstanceActivity類時(shí),會(huì)出現(xiàn)一個(gè)Instance View窗口,該窗口完整得記錄了該對(duì)象在這一段時(shí)間內(nèi)的分配情況,如下圖所示,Instance View窗口中顯示了7個(gè)SingleInstanceActivity對(duì)象,并記錄了每個(gè)對(duì)象被分配(Alloc Time)、釋放(Dealloc Time)的時(shí)間。

          但是當(dāng)我們強(qiáng)制GC后,仍然還存在兩個(gè)SingleInstanceActivity對(duì)象,根據(jù)平時(shí)的開發(fā)經(jīng)驗(yàn),其中的一個(gè)對(duì)象可能被某個(gè)對(duì)象持有,導(dǎo)致無法被釋放從而造成泄漏。
          (3) 如果我們希望確定(2)中無法被GC的對(duì)象被誰持有,可以點(diǎn)擊該對(duì)象,此時(shí)在Instance View窗口的下方就會(huì)出現(xiàn)Allocation Call Stack標(biāo)簽,如上圖藍(lán)色方框所示,該標(biāo)簽中顯示了該對(duì)象被分配到何處以及哪里線程中,此外,我們還可以在標(biāo)簽中右鍵點(diǎn)擊任意行并選擇Jump to Source,以在編輯器中打開該代碼。

          2.1.2 Heap Dump
          ?
          Head Dump,即捕獲堆轉(zhuǎn)儲(chǔ),它的作用是捕獲某一時(shí)刻應(yīng)用中有哪些對(duì)象正在使用內(nèi)存,并將這些信息存儲(chǔ)到文件中。Head Dump可以幫助我們找到大對(duì)象和通過數(shù)據(jù)的變化發(fā)現(xiàn)內(nèi)存泄漏,比如當(dāng)我們的應(yīng)用使用一段時(shí)候后,捕獲了一個(gè)heap dump,這個(gè)heap dump里面發(fā)現(xiàn)了并不應(yīng)該存在的對(duì)象分配情況,這說明是存在內(nèi)存泄漏的。捕獲堆轉(zhuǎn)儲(chǔ)后,可以查看以下信息:

          • 該時(shí)刻應(yīng)用分配了哪些類型的對(duì)象,每種對(duì)象有多少;
          • 每個(gè)對(duì)象當(dāng)前時(shí)刻使用了多少內(nèi)存;
          • 對(duì)象所分配到的調(diào)用堆棧(Android 7.1以下會(huì)有所區(qū)別);
          ?
          要捕獲堆轉(zhuǎn)儲(chǔ),通過點(diǎn)擊 Memory Profiler 工具欄中的 Dump Java heap圖標(biāo) 實(shí)現(xiàn),獲得某一時(shí)刻的Heap Dump如下圖:
          接下來,我們?nèi)匀灰陨弦黄恼轮兴峒暗膯卫J揭鸬膬?nèi)存泄漏為例,來分析Heap Dump所表達(dá)的信息。從下圖展示內(nèi)容可看出,Heap Dump表達(dá)的窗體與Allocation Tracker差不多,只是展示的具體內(nèi)容不同。具體如下圖所示:

          下面我們解釋下上圖顏色方框中相關(guān)標(biāo)簽名表示的意義。

          (1) 紅色方框
          • Allocations: 堆中分配對(duì)象的個(gè)數(shù);
          • Native Size:此對(duì)象類型使用的native內(nèi)存總量。此列僅適用于Android 7.0及更高版本。您將在這里看到一些用Java分配內(nèi)存的對(duì)象,因?yàn)锳ndroid使用native內(nèi)存來處理某些框架類,例如Bitmap。
          • Shallow Size: 此對(duì)象類型使用的Java內(nèi)存總量;
          • Retained Size: 因此類的所有實(shí)例而保留的內(nèi)存總大??;

          (2) 黃色方框
          • Depth:從任意 GC root 到所選實(shí)例的最短 hop 數(shù)。
          • Native Size: native內(nèi)存中此實(shí)例的大小。此列僅適用于Android 7.0及更高版本。
          • Shallow Size:此實(shí)例Java內(nèi)存的大小。
          • Retained Size:此實(shí)例支配[dominator]的內(nèi)存大?。ǜ鶕?jù) [支配樹]

          2.2 MAT
          ?
          在進(jìn)行內(nèi)存分析時(shí),我們可以使用Android Profiler的Memory Profiler組件來觀察、追蹤內(nèi)存的分配使用情況(Allocation Tracker),也可以通過這個(gè)工具找到疑似發(fā)生內(nèi)存泄漏的位置(Heap Dump)。但是如果想要深入地進(jìn)行分析并確定內(nèi)存泄漏,就要分析疑似發(fā)生內(nèi)存泄漏時(shí)所生產(chǎn)的堆轉(zhuǎn)儲(chǔ)文件,該文件由點(diǎn)擊 Memory Profiler工具欄中的 Dump Java heap圖標(biāo) 生成,輸出的文件格式為hprof,分析工具使用的是MAT。由于Memory Profiler生成的hprof文件不是標(biāo)準(zhǔn)的hprof文件,需要使用SDK自帶的hprof-conv進(jìn)行轉(zhuǎn)換,它的路徑在sdk/platform-tools中,執(zhí)行命令:hprof-conv E:\1.hprof E:\standar.hprof。

          MAT,全稱"Memory Analysis Tool",是對(duì)內(nèi)存進(jìn)行詳細(xì)分析的工具,它是eclipse的一個(gè)插件,對(duì)于AS開發(fā)來說,需要單獨(dú)下載MAT(當(dāng)前最新版本為1.9.1)。

          使用MAT打開一個(gè)標(biāo)準(zhǔn)的hprof文件如上圖所示,選擇Leak Suspects Report選項(xiàng),MAT為hprof文件生成的報(bào)告,該報(bào)告為中給出了MAT認(rèn)為可能出現(xiàn)內(nèi)存泄漏問題的地方,除非內(nèi)存泄漏特別明顯,通過Leak Suspects還是很難發(fā)現(xiàn)內(nèi)存泄漏的位置。

          因此,我們還是老老實(shí)實(shí)自己來動(dòng)手分析,這里打開Overview標(biāo)簽(一般打開文件時(shí)會(huì)自動(dòng)打開),具體如下圖:

          在上述圖中,我們主要關(guān)注兩個(gè)部分:餅狀圖和Actions,其中,餅狀圖主要用來顯示內(nèi)存的消耗,它的彩色部分表示被分配的內(nèi)存,灰色部分則是空閑區(qū)域,單擊每個(gè)彩色區(qū)域可以看到這塊區(qū)域的詳細(xì)信息;Actons一欄列出了4種Action,其作用與區(qū)別如下。

          • Historgram:列出每個(gè)類的所有對(duì)象。從類的角度進(jìn)行分析,注重量的分析;
          • Dominator Tree:列出大對(duì)象和它們的引用關(guān)系。從對(duì)象的角度分析,注重引用關(guān)系分析;
          • Top Consumers:獲取開銷最大的對(duì)象,可通過類或包形式分組;
          • Duplicate Classes:檢測(cè)出被多個(gè)類加載器加載的類;
          ?
          其中,分析內(nèi)存泄漏最常用的就是Histogram和Dominator Tree。這兩種方式只是判斷內(nèi)存問題的方式不同,但是最終都會(huì)歸結(jié)到通過查看GC引用鏈來確定內(nèi)存泄漏的具體位置(原因)。

          接下來,我們就以Dominator Tree為例來講解如何使用MAT來判定是否有內(nèi)存泄漏以及泄漏的具體原因。

          Dominator Tree,即支配樹,點(diǎn)擊Dominator Tree選項(xiàng)如下圖所示,然后使用條件過濾(黃色方框輸入),找一個(gè)我們認(rèn)為可能發(fā)生了內(nèi)存泄漏的類:


          從上圖可以看到,在Dominator Tree列出了很多SingleInstanceActivity的實(shí)例,而一般SingleInstanceActivity是不該有這么多實(shí)例的,因此,基本可以斷定發(fā)生了內(nèi)存泄漏,至于內(nèi)存泄漏的具體原因,就需要查看GC引用鏈。但在查看之前,我們需要理解下紅色方框幾個(gè)標(biāo)簽的意義。

          • Shallow Heap
          ?
          對(duì)象自身占用的內(nèi)存大小,不包括引用的對(duì)象。如果是數(shù)組類型的對(duì)象,它的大小由數(shù)組元素的類型和長度決定;如果是非數(shù)組類型的對(duì)象,它的大小由其成員變量的數(shù)量和類型決定。

          • Retained Heap
          ?
          Retained Heap就是當(dāng)前對(duì)象被GC后,從Heap上總共能釋放掉多大的內(nèi)存空間,這部分內(nèi)存空間被稱之為Retained Set。Retained Set指的是這個(gè)對(duì)象本身和它持有引用的對(duì)象以及這些引用對(duì)象的Retained Set所占內(nèi)存大小的總和。下面我們從一顆引用樹(GC引用鏈)來理解下Retained Set:

          ?
          假設(shè)A、B為GC Root對(duì)象,根據(jù)Retained Set定義可知,對(duì)象E的Retained Set為對(duì)象E、G,對(duì)象C的Retained Set為對(duì)象C、D、E、F、G、H。另外,通過引用樹我們還可以演化得到本小節(jié)最重要的部分–支配樹(Dominator Tree),即在引用樹中如果一條到對(duì)象Y的路徑一定(必然)會(huì)經(jīng)過對(duì)象X,那么稱為X支配Y,并且在所有支配Y的對(duì)象中,X是Y最近的一個(gè)對(duì)象就稱為X直接支配Y,支配樹就是反應(yīng)的這種直接支配關(guān)系。

          在支配樹中,父節(jié)點(diǎn)直接支配子節(jié)點(diǎn)。上圖就是引用樹轉(zhuǎn)換為支配樹的例子,由此可以得到:

          • 對(duì)象C直接支配對(duì)象D、E、H,故C是D、E、H的父節(jié)點(diǎn);

          • 對(duì)象D直接支配對(duì)象F,故D是F的父節(jié)點(diǎn);

          • 對(duì)象E直接支配對(duì)象G,故E是G的父節(jié)點(diǎn);

          ?
          通過支配樹,我們可以知道假如對(duì)象E被回收了,則會(huì)釋放E、G的內(nèi)存,而不會(huì)釋放H的內(nèi)存,因?yàn)镕可能還會(huì)引用著H,只有C被回收了,H的內(nèi)存才會(huì)被釋放。

          因此,我們可以得到一個(gè)結(jié)論:通過MAT的Dominator Tree,可以清晰地得
          到一個(gè)對(duì)象的直接支配的對(duì)象,如果直接支配對(duì)象中出現(xiàn)了不該有的對(duì)象,就說明發(fā)生了內(nèi)存泄漏。示例如下:



          從上圖可知,被選中的SingleInstanceActivity對(duì)象的直接支配對(duì)象出現(xiàn)了不該有的CommonUtils對(duì)象,因?yàn)镾ingleInstanceActivity是要被回收的。換句話說,CommonUtils持有SingleInstanceActivity對(duì)象的引用,導(dǎo)致SingleInstanceActivity對(duì)象無法被正?;厥眨瑥亩鴮?dǎo)致了內(nèi)存泄漏。

          2.3 LeakCanary
          ?
          文章最后,我們借助Dalvik 虛擬機(jī)和 Sun JVM 在架構(gòu)和執(zhí)行方面有什么本質(zhì)區(qū)別?一文中的一段話作個(gè)總結(jié),個(gè)人覺得這對(duì)理解JVM/Dalvik/ART的本質(zhì)比較有啟發(fā)意義,即JVM其核心目的,是為了構(gòu)建一個(gè)真正跨OS平臺(tái),跨指令集的程序運(yùn)行環(huán)境(VM)。

          DVM的目的是為了將android OS的本地資源和環(huán)境,以一種統(tǒng)一的界面提供給應(yīng)用程序開發(fā)。嚴(yán)格來說,DVM不是真正的VM,它只是開發(fā)的時(shí)候提供了VM的環(huán)境,并不是在運(yùn)行的時(shí)候提供真正的VM容器。

          這也是為什么JVM必須設(shè)計(jì)成stack-based的原因。




          瀏覽 78
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产资源在线观看 | 亚洲高清网站 | 亚洲AV无MM码性色AV无码网站HMM | 香蕉网狼人| 激情五月丁香婷婷 |