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

        共 17537字,需瀏覽 36分鐘

         ·

        2021-03-14 11:21



        Android虛擬機:Dalvik和ART


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

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

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

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

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

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

        ?

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

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

        1.1 JVM與Dalvik區(qū)別

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

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

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

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

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

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

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

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

        ?

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


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

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


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


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


        (4) Dalvik擁有Zygote進程與共享機制

        ?

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


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

        1.2 Dalvik與ART區(qū)別


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


        (1) ART運行機制優(yōu)于Dalvik

        ?

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


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

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

        ?

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


        (3) 運行時堆劃分不同

        ?

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


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


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


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


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

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

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

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

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

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

        當(dāng)Android系統(tǒng)在啟動init進程后,該進程會孵化出一堆用戶守護進程、ServiceManager服務(wù)以及Zygote進程等等,其中,Zygote進程是Android系統(tǒng)啟動的第一個Java進程,或者說是虛擬機進程,因為它持有Dalvik或ART的實例。

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

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

        //app_main.cpp$main函數(shù)int main(int argc, char* const argv[]){    ...    // (1) 創(chuàng)建AppRuntime對象    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
        ... // (2) 解析執(zhí)行init.rc的啟動服務(wù)的命令傳入的參數(shù) // 解析后:zygote = true // startSystemServer = true // niceName = zygote (當(dāng)前進程名稱) 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è)置進程名為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ù)源碼可知,為了啟動Zygote進程,該函數(shù)主要做個如下三個方面的工作,即:

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

        class AppRuntime : public AndroidRuntime{};


        • 解析執(zhí)行init.rc的啟動服務(wù)的命令傳入的參數(shù)。/init.zygote64_32.rc文件中啟動Zygote的內(nèi)容如下,在<Android源代碼目錄>/system/core/rootdir/ 目錄下可以看到init.zygote32.rc、init.zygote32_64.rc、init.zygote64.rc、init.zygote64_32.rc等文件,這是因為Android5.0開始支持64位的編譯,所以Zygote進程本身也有32位和64位版本。啟動Zygote進程命令如下:
          /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)前程序運行的進程是Zygote進程,將調(diào)用AppRuntime的start函數(shù)執(zhí)行ZygoteInit類,從類名可以看出執(zhí)行該類將進入Zygote的初始化流程。

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

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

        該函數(shù)主要完成三個方面的工作:(a) 初始化JNI環(huán)境,啟動虛擬機;(b) 為虛擬機注冊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)境、啟動虛擬機    JniInvocation jni_invocation;    jni_invocation.Init(NULL);    JNIEnv* env;    if (startVm(&mJavaVM, &env, zygote) != 0) {        return;    }    onVmCreated(env);
        // (2) 為虛擬機注冊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的方法,到這里我們就進入了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進程是如何啟動Zygote進程和在Zygote進程中創(chuàng)建虛擬機的實例的這個過程我們就分析完畢了,也驗證了Zygote進程在被創(chuàng)建啟動后,確實已經(jīng)持有了虛擬機的實例,以至于Zygote進程fork自身創(chuàng)建應(yīng)用程序時,應(yīng)用程序也得到了虛擬機的實例,這樣就不需要每次啟動應(yīng)用程序進程都要創(chuàng)建虛擬機實例,從而加快了應(yīng)用程序進程的啟動速度。

        至于被創(chuàng)建的是Dalvik還是ART實例,我們可以看注釋(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平臺標(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ù)源碼可知,它首先會調(diào)用JniInvocation::GetLibrary函數(shù)來獲取要指定的虛擬機庫名稱–“l(fā)ibart.so”或“l(fā)ibdvm.so”,然后調(diào)用JniInvocation::dlopen函數(shù)加載這個虛擬機庫。


        通過查閱JniInvocation::GetLibrary函數(shù)源碼可知,如果當(dāng)前不是Debug模式構(gòu)建的,是不允許動態(tài)更改虛擬機動態(tài)庫,即默認為"libart.so";如果當(dāng)前是Debug模式構(gòu)建且傳入的buffer不為NULL時,就需要通過讀取"persist.sys.dalvik.vm.lib.2"這個系統(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)建,不允許更改虛擬機動態(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)的實時變化。

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

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

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

          • 一個堆疊圖表。顯示每個內(nèi)存類別當(dāng)前使用多少內(nèi)存,如左側(cè)的y軸和頂部的彩色健所示。
          • Java:從Java或Kotlin代碼分配的對象的內(nèi)存(重點關(guān)注);
          • Native:從C或C++代碼分配的對象的內(nèi)存(重點關(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代碼分配的對象數(shù)。
          • 一個堆疊圖表。顯示每個內(nèi)存類別當(dāng)前使用多少內(nèi)存,如左側(cè)的y軸和頂部的彩色健所示。

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


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


        2.1.1 Allocation Tracker

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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



        從上圖可知,被選中的SingleInstanceActivity對象的直接支配對象出現(xiàn)了不該有的CommonUtils對象,因為SingleInstanceActivity是要被回收的。換句話說,CommonUtils持有SingleInstanceActivity對象的引用,導(dǎo)致SingleInstanceActivity對象無法被正常回收,從而導(dǎo)致了內(nèi)存泄漏。

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

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

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




        瀏覽 78
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 大香蕉狠狠| 日本亲与子乱人妻hd | 99免费在线视频 | 久久久天堂 | 波多野结衣光棍影院 |