Android性能優化(2):常見內存泄漏與優化(二)


Android性能優化(1):常見內存泄漏與優化(一)一文中,我們詳細闡述了Java虛擬機工作原理和Android開發中常見的內存泄漏及其優化方法,本文將在此基礎上繼續學習Android虛擬機發展歷程、Dalvik/ART的運行時堆、Dalvik/ART啓動流程以及常見的內存分析工具的特點和使用方法,包括Android Profiler、MAT、LeakCanary等。

1. Android虛擬機:Dalvik和ART

 Dalvik是Google特別設計專門用於Android平臺的虛擬機,它位於Android系統架構的Android的運行環境(Android Runtime)中,是Android移動設備平臺的核心組成部分之一。類似於傳統的JVMDalvik虛擬機是在Android操作系統上虛擬出來的一個“設備”,用來運行Android應用程序(APP),主要負責堆棧管理、線程管理、安全及異常管理、垃圾回收、對象的生命週期管理等。在Android系統中,每一個APP對應着一個Dalvik虛擬機實例。Dalvik虛擬機支持.dex(即"Dalvik Executable")格式的Java應用程序的運行,.dex是專爲Dalvik設計的一種壓縮格式,它是在.class字節碼文件的基礎上經過DEX工具壓縮、優化後得到的,適用於內存和處理器速度有限的系統。在Android 4.4以前的系統中,Android系統均採用Dalvik作爲運行Android應用的虛擬機,但隨着Dalvik的不足逐漸暴露,到Android 5.0以後的系統使用ART虛擬機完全取代了Dalvik虛擬機。ART虛擬機在性能上做了很多優化,比如採用預編譯(AOT,Ahead Of Time compilation)取代JIT編譯器、支持64位CPU、改進垃圾回收機制等等,從而使得Android系統運行更爲流暢。下圖展示了Android系統架構和DVM架構:
在這裏插入圖片描述
 Android虛擬機的使用,使得Android應用和Linux內核分離,從而使得Android系統更加穩定可靠,也就是說,即便是某個Android程序被嵌入了惡意代碼,也不會直接影響系統的正常運行。接下來,我們就從分析JVM、Dalvik、ART三者之間的關係,來進一步瞭解它們。

1.1 JVM與Dalvik區別

 在Android 4.4以前,Android中的所有Java程序都是運行在Dalvik虛擬機上的,每個Android應用進程對應着一個獨立的Dalvik虛擬機實例並在其解釋下執行。雖然Dalvik虛擬機也是用來運行Java程序,但是它並沒有遵守Java虛擬機規範來實現,是Google爲Android平臺特殊設計且運行在Android 運行時庫的虛擬機,因此Dalvik虛擬機並不是一個Java虛擬機。它們的主要區別如下:

  • (1) 基於的架構不同

JVM基於棧架構,Dalvik虛擬機基於寄存器架構。JVM基於棧則意味着需要去棧中讀寫數據,所需更多的指令會更多(主要是load/store指令),這樣會導致速度變慢,對於性能有限的移動設備,顯然是不合適的;Dalvik虛擬機基於寄存器實現,則意味着它的指令會更加緊湊、簡潔,因爲虛擬機在複製數據時不需要使用大量的出入棧指令,但由於需要指定源地址和目標地址,所以基於寄存器的指令會比基於棧的指令要大,當然,由於指令數量的減少,總的代碼數不會增加多少。下圖的一段Java程序代碼,展示了在JVM和Dalvik虛擬機中字節碼的表現形式:
在這裏插入圖片描述

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

  • (2) 執行的字節碼文件不同

JVM運行的.class文件,Dalvik運行的是.dex(即Dalvik Executable)文件。在Java程序中,Java類會編譯成一個或多個.class文件,然後打包到.jar文件中,.jar文件中的每個.class文件裏面包含了該類的常量池、類信息、屬性等。當JVM加載該.jar文件時,會加載裏面的所有的.class文件,JVM的這種加載方式很慢,對於內存有限的移動設備並不合適;.dex文件是在.class文件的基礎上,經過DEX工具壓縮和優化後形成的,通常每一個.apk文件中只包含了一個.dex,這個.dex文件將所有的.class裏面所包含的信息全部整合在一起了,這樣做的好處就是減少了整體的文件尺寸(去除了.class文件中相同的冗餘信息),同時減少了I/O操作加快了類的查找速度。下圖展示了.jar和.dex的對比差異:
在這裏插入圖片描述

  • (3) 在內存中的表現形式差異

Dalvik經過優化,允許在有限的內存中同時運行多個進程,或說同時運行多個Dalvik虛擬機的實例。在Android中每一個應用都運行在一個Dalvik虛擬機實例中,每一個Dalvik虛擬機實例都運行在一個獨立的進程空間中,因此都對應着一個獨立的進程,獨立的進程可以防止在虛擬機崩潰時所有程序都被關閉。而對於JVM來說,在其宿主OS的內存中只運行着一個JVM的實例,這個JVM實例中可以運行多個Java應用程序(進程),但是一旦JVM異常崩潰,就會導致運行在其中的所有程序被關閉。

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

 在Android系統中有個一特殊的虛擬機進程--Zygote,它是虛擬機實例的孵化器。它在Android系統啓動的時候就會產生,完成虛擬機的初始化、庫的加載、預製類庫和初始化操作。如果系統需要一個新的虛擬機實例,他會迅速複製自身,以最快的速度提供給系統。對於一些只讀的系統庫,所有的虛擬機實例都和Zygote共享一塊區域。 Dalvik虛擬機擁有預加載-共享的機制,使得不同的應用之間在運行時可以共享相同的類,因此擁有更高的效率。而JVM則不存在這個共享機制,不同的程序被打包後都是彼此獨立的,即便它們在包裏使用了相同的類,運行時的都是單獨加載和運行,無法進行共享。
在這裏插入圖片描述

1.2 Dalvik與ART區別

ART虛擬機被引入於Android 4.4,用來替換Dalvik虛擬機,以緩解Dalvik虛擬機的運行機制導致Android應用運行變慢的問題。在Android 4.4中,可以選擇使用Dalvik還是ART,而從Android 5.0開始,Dalvik被完全刪除,Android系統默認採用ART。Dalvik與ART的主要區別如下:

  • (1) ART運行機制優於Dalvik

 對於運行在Dalvik虛擬機實例中的應用程序而言,在每一次重新運行的時候,都需要將字節碼通過JIT(Just-In-Time)編譯器編譯成機器碼,這會使用應用程序的運行效率降低,雖然Dalvik虛擬機已經被做過很多優化(.dex文件->.odex文件),但由於這種先翻譯再執行的機制仍然無法有效解決Dalvik拖慢Android應用運行的事實。而在ART中,系統在安裝應用程序時會進行一次AOT(Ahead Of Time compilication,預編譯),即將字節碼預先編譯成機器碼並存儲在本地,這樣應用程序每次運行時就不需要執行編譯了,運行效率會大大提高。
在這裏插入圖片描述

  • (2) 支持的CPU架構不同

 Dalvik是爲32位CPU設計的,而ART支持64位併兼容32位的CPU。

 Dalvik虛擬機的運行時堆使用標記--清除(Mark--Sweep)算法進行GC,它由兩個Space以及多個輔助數據結構組成,兩個Space分別是Zygote Space(Zygote Heap)Allocation Space(Active Heap)Zygote Space用來管理Zygote進程在啓動過程中預加載和創建的各種對象,Zygote Space中不會觸發GC,應用進程和Zygote進程之間會共享Zygote Space。Zygote進程在fork第一個子進程之前,會把Zygote Space分爲兩個部分,原來被Zygote進程使用的部分仍然叫Zygote Space,而剩餘未被使用的部分被稱爲Allocation Space,以後fork的子進程相關的所有的對象都會在Allocation Space上進行分配和釋放。需要注意的是,Allocation Space不是進程共享的,在每個進程中都獨立擁有一份。下圖展示了Dalvik虛擬機的運行時堆結構:

 與Dalvik的GC不同,ART採用了多種垃圾收集方案,每個方案會運行不同的垃圾收集器,默認是採用了CMS(Concurrent Mark-Sweep)方案,該方案主要使用了sticky-CMS和patial-CMS。根據不同的CMS方案,ART的運行時堆的空間劃分也會不同,默認是由4個Space和多個輔助數據結構組成,4個Space分別是Zygote SpaceAllocation SpaceImage Space以及Large Object Space,其中,Zygote SpaceAllocation Space和Dalvik的一樣,Image Space用來存放一些預加載類,Large Object Space用來分配一些大對象(默認大小爲12Kb)。需要注意的是,Zygote SpaceImage Space是進程間共享的。下圖展示了ART採用標記–清除算法的堆劃分:


ART虛擬機的不足:

  • 安裝時間變長。應用在安裝的時候需要預編譯,從而增大了安裝時間。
  • 存儲空間變大。ART引入AOT技術後,需要更多的空間存儲預編譯後的機器碼。

 因此,從某些程度上來說,ART虛擬機是利用了“空間換時間”,來提高Android應用的運行速度。爲了緩解上述的不足,Android7.0(N)版本中的ART加入了即時編譯器JIT,作爲AOT的一個補充,在應用程序安裝時並不會將字節碼全部編譯成機器碼,而是在運行中將熱點代碼編譯成機器碼,具體來說,就是當我們第一次運行應用相關程序後,JIT提供了一套追蹤機制來決定哪一部分代碼需要在手機處於idle狀態和充電的時候來編譯,並將編譯得到的機器碼存儲到本地,這個追蹤技術被稱爲Profile Guided Compilcation

1.3 Dalvik/ART的啓動流程

 從剖析Android系統的啓動過程一文可知,init進程(pid=1)是Linux系統的用戶進程,是所有用戶進程的父進程。當Android系統在啓動init進程後,該進程會孵化出一堆用戶守護進程、ServiceManager服務以及Zygote進程等等,其中,Zygote進程是Android系統啓動的第一個Java進程,或者說是虛擬機進程,因爲它持有Dalvik或ART的實例。Zygote進程是所有Java進程的父進程,每當系統需要創建一個應用程序時,Zygote進程就會fork自身,並快速地創建和初始化一個虛擬機實例,用於應用程序的運行。下面我們就從Android9.0源碼的角度,來分析Zygote進程中的Dalvik或ART虛擬機實例是如何創建的。

 首先,根據init.rc的啓動服務的命令,將運行/system/bin/app_process可執行程序來啓動zygote進程,該可執行程序對應的源文件爲../base/cmds/app_process/app_main.cpp,也就是說,當從init進程發起啓動Zygote進程後,會調用app_main.cppmain函數進入Zygote進程的啓動流程。app_main.cpp的main函數源碼如下:

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

  • 創建AppRuntime實例。AppRuntime是在app_process.cpp中定義的類,繼承於系統的AndroidRuntime,主要用於創建和初始化虛擬機。AppRuntime類繼承關係如下:
class AppRuntime : public AndroidRuntime
{};
  • 解析執行init.rc的啓動服務的命令傳入的參數。/init.zygote64_32.rc文件中啓動Zygote的內容如下,在<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進程命令如下:

    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/tasks
    
  • 執行ZygoteInit類。由前面 解析命令傳入的參數可知,zygote=true說明當前程序運行的進程是Zygote進程,將調用AppRuntime的start函數執行ZygoteInit類,從類名可以看出執行該類將進入Zygote的初始化流程。

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

 接着,我們詳細分析下AppRuntime的start函數執行流程。由於AppRuntime繼承於AndroidRuntime,因此start函數具體在AndroidRuntime中實現。該函數主要完成三個方面的工作:(a) 初始化JNI環境,啓動虛擬機;(b) 爲虛擬機註冊JNI方法;(c)從傳入的com.android.internal.os.ZygoteInit 類中找到main函數,即調用ZygoteInit.java類中的main方法。AndroidRuntime$start源碼如下:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    ...
    // (1) 初始化JNI環境、啓動虛擬機
    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函數,即調用	
    // ZygoteInit.java類中的main方法。AndroidRuntime及之前的方法都是native的方法,而此刻  	
        // 調用的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函數被調用,Init進程是如何啓動Zygote進程和在Zygote進程中創建虛擬機的實例的這個過程我們就分析完畢了,也驗證了Zygote進程在被創建啓動後,確實已經持有了虛擬機的實例,以至於Zygote進程fork自身創建應用程序時,應用程序也得到了虛擬機的實例,這樣就不需要每次啓動應用程序進程都要創建虛擬機實例,從而加快了應用程序進程的啓動速度。至於被創建的是Dalvik還是ART實例,我們可以看註釋(1)處調用了jni_invocationInit()函數,該函數源碼如下,位於源碼根目錄下libnativehelper/Jnilnvocation.cpp源文件中。該函數源碼如下:

#ifdef __ANDROID__
#include <sys/system_properties.h>
#endif

// JniInvocation::Init
bool JniInvocation::Init(const char* library) {
    // Android平臺標誌
    #ifdef __ANDROID__
      char buffer[PROP_VALUE_MAX];
    #else
      char* buffer = NULL;
    #endif
      // 獲取“libart.so”或“libdvm.so”
      library = GetLibrary(library, buffer);
      const int kDlopenFlags = RTLD_NOW | RTLD_NODELETE;
      // 加載“libart.so”或“libdvm.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函數源碼可知,它首先會調用JniInvocation::GetLibrary函數來獲取要指定的虛擬機庫名稱–“libart.so”或“libdvm.so”,然後調用JniInvocation::dlopen函數加載這個虛擬機庫。通過查閱JniInvocation::GetLibrary函數源碼可知,如果當前不是Debug模式構建的,是不允許動態更改虛擬機動態庫,即默認爲"libart.so";如果當前是Debug模式構建且傳入的buffer不爲NULL時,就需要通過讀取"persist.sys.dalvik.vm.lib.2"這個系統屬性來設置返回的library。JniInvocation::GetLibrary函數源碼如下:

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構建,不允許更改虛擬機動態庫
  // library = default_library = kLibraryFallback = "libart.so"
  if (!is_debuggable()) {
    library = kLibraryFallback;
    default_library = kLibraryFallback;
  } else {
    // 如果是debug構建,需要判斷傳入的buffer參數是否爲空
    // 如果不爲空,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"是系統屬性
// 它的取值可以爲libdvm.so或libart.so
int GetLibrarySystemProperty(char* buffer) {
#ifdef __ANDROID__
  return __system_property_get("persist.sys.dalvik.vm.lib.2", buffer);
#else
  UNUSED(buffer);
  return 0;
#endif
}

2. 常見內存分析工具

2.1 Android Profiler

Android Profiler引入於Android Studio 3.0,是用來替換之前的Android Monitor,主要用來觀察內存(Memory)、網絡(Network)、CPU使用狀態的實時變化。這裏我們主要介紹Android Profiler中的Memory Profiler組件,它對應於Android Monitor的Memory Monitor,通過Memory Profiler可以實時查看/捕獲存儲內存的使用狀態強制GC以及跟蹤內存分配情況,以便於快速地識別可能會導致應用卡頓、凍結甚至崩潰的內存泄漏和內存抖動。我們可以通過依次點擊AS控制面板的View->Tool Windows->Profiler或者點擊左下角的圖標,進入Memory Profiler監控面板。

在這裏插入圖片描述

  • 標註(1~6)說明

    1:用於強制執行垃圾回收事件的按鈕;
    2:用於捕獲堆轉儲的按鈕,即Dump the Java heap;
    3:用於放大、縮小、復位時間軸的按鈕;
    4 :用於實時播放內存分配情況的按鈕;
    5:發生一些事件的記錄(如Activity的跳轉,事件的輸入,屏幕的旋轉);
    6:內存使用量事件軸,它包括以下內容:

    • 一個堆疊圖表。顯示每個內存類別當前使用多少內存,如左側的y軸和頂部的彩色健所示。
      • Java:從Java或Kotlin代碼分配的對象的內存(重點關注);
      • Native:從C或C++代碼分配的對象的內存(重點關注);
      • Graphics:圖像緩存等,包括GL surfaces, GL textures等;
      • Stack:棧內存(包括java和c/c++);
      • Code:用於處理代碼和資源(如 dex 字節碼.so 庫和字體)分配的內存;
      • Other:系統都不知道是什麼類型的內存,放在這裏;
      • Allocated:從Java或Kotlin代碼分配的對象數。
    • 一條虛線。虛線表示分配的對象數量,如右側的y軸所示(5000/15000)。
    • 每個垃圾回收時間的圖標。
2.1.1 Allocation Tracker

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

  • 分配了哪些類型的對象,分配了多大的空間;
  • 對象分配的棧調用,是在哪個線程中調用的;
  • 對象的釋放時間(只針對8.0+);

 如果是Android 8.0以上的設備,支持隨時查看對象的分配情況,具體的步驟如下:在時間軸上拖動以選擇要查看的哪個區域(時間段)的內存分配情況,如下圖所示:
在這裏插入圖片描述
 接下來,我們就以上一篇文章中所提及的單例模式引起的內存泄漏爲例,來檢查內存分配的記錄,排查可能存在內存泄漏的對象。具體的步驟如下:

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

  • Allocations:堆中動態分配對象個數;
  • Deallocations:解除分配的對象個數;
  • Total Counts:目前存在的對象總數;
  • Shallow Size:堆中所有對象的總大小(以字節爲單位),不包含其引用的對象;

 (2) 當點擊SingleInstanceActivity類時,會出現一個Instance View窗口,該窗口完整得記錄了該對象在這一段時間內的分配情況,如下圖所示,Instance View窗口中顯示了7個SingleInstanceActivity對象,並記錄了每個對象被分配(Alloc Time)、釋放(Dealloc Time)的時間。但是當我們強制GC後,仍然還存在兩個SingleInstanceActivity對象,根據平時的開發經驗,其中的一個對象可能被某個對象持有,導致無法被釋放從而造成泄漏。
在這裏插入圖片描述
  (3) 如果我們希望確定(2)中無法被GC的對象被誰持有,可以點擊該對象,此時在Instance View窗口的下方就會出現Allocation Call Stack標籤,如上圖藍色方框所示,該標籤中顯示了該對象被分配到何處以及哪裏線程中,此外,我們還可以在標籤中右鍵點擊任意行並選擇Jump to Source,以在編輯器中打開該代碼。

2.1.2 Heap Dump

Head Dump,即捕獲堆轉儲,它的作用是捕獲某一時刻應用中有哪些對象正在使用內存,並將這些信息存儲到文件中。Head Dump可以幫助我們找到大對象和通過數據的變化發現內存泄漏,比如當我們的應用使用一段時候後,捕獲了一個heap dump,這個heap dump裏面發現了並不應該存在的對象分配情況,這說明是存在內存泄漏的。捕獲堆轉儲後,可以查看以下信息:

  • 該時刻應用分配了哪些類型的對象,每種對象有多少;
  • 每個對象當前時刻使用了多少內存;
  • 對象所分配到的調用堆棧(Android 7.1以下會有所區別);
     要捕獲堆轉儲,通過點擊 Memory Profiler 工具欄中的 Dump Java heap圖標 img實現,獲得某一時刻的Heap Dump如下圖:
    在這裏插入圖片描述
     接下來,我們仍然以上一篇文章中所提及的單例模式引起的內存泄漏爲例,來分析Heap Dump所表達的信息。從下圖展示內容可看出,Heap Dump表達的窗體與Allocation Tracker差不多,只是展示的具體內容不同。具體如下圖所示:
    在這裏插入圖片描述
     下面我們解釋下上圖顏色方框中相關標籤名錶示的意義。

(1) 紅色方框

  • Allocations: 堆中分配對象的個數;
  • Native Size: 此對象類型使用的native內存總量。 此列僅適用於Android 7.0及更高版本。您將在這裏看到一些用Java分配內存的對象,因爲Android使用native內存來處理某些框架類,例如Bitmap。
  • Shallow Size: 此對象類型使用的Java內存總量;
  • Retained Size: 因此類的所有實例而保留的內存總大小;

(2) 黃色方框

  • Depth:從任意 GC root 到所選實例的最短 hop 數。
  • Native Size: native內存中此實例的大小。此列僅適用於Android 7.0及更高版本。
  • Shallow Size:此實例Java內存的大小。
  • Retained Size:此實例支配[dominator]的內存大小(根據 [支配樹]
2.2 MAT

 在進行內存分析時,我們可以使用Android Profiler的Memory Profiler組件來觀察、追蹤內存的分配使用情況(Allocation Tracker),也可以通過這個工具找到疑似發生內存泄漏的位置(Heap Dump)。但是如果想要深入地進行分析並確定內存泄漏,就要分析疑似發生內存泄漏時所生產的堆轉儲文件,該文件由點擊 Memory Profiler工具欄中的 Dump Java heap圖標 img生成,輸出的文件格式爲hprof,分析工具使用的是MAT。由於Memory Profiler生成的hprof文件不是標準的hprof文件,需要使用SDK自帶的hprof-conv進行轉換,它的路徑在sdk/platform-tools中,執行命令:hprof-conv E:\1.hprof E:\standar.hprof
在這裏插入圖片描述
MAT,全稱"Memory Analysis Tool",是對內存進行詳細分析的工具,它是eclipse的一個插件,對於AS開發來說,需要單獨下載MAT(當前最新版本爲1.9.1)。使用MAT打開一個標準的hprof文件如上圖所示,選擇Leak Suspects Report選項,MAT爲hprof文件生成的報告,該報告爲中給出了MAT認爲可能出現內存泄漏問題的地方,除非內存泄漏特別明顯,通過Leak Suspects還是很難發現內存泄漏的位置。因此,我們還是老老實實自己來動手分析,這裏打開Overview標籤(一般打開文件時會自動打開),具體如下圖:
在這裏插入圖片描述
 在上述圖中,我們主要關注兩個部分:餅狀圖和Actions,其中,餅狀圖主要用來顯示內存的消耗,它的彩色部分表示被分配的內存,灰色部分則是空閒區域,單擊每個彩色區域可以看到這塊區域的詳細信息;Actons一欄列出了4種Action,其作用與區別如下。

  • Historgram:列出每個類的所有對象。從類的角度進行分析,注重量的分析;
  • Dominator Tree:列出大對象和它們的引用關係。從對象的角度分析,注重引用關係分析;
  • Top Consumers:獲取開銷最大的對象,可通過類或包形式分組;
  • Duplicate Classes:檢測出被多個類加載器加載的類;

 其中,分析內存泄漏最常用的就是HistogramDominator Tree。這兩種方式只是判斷內存問題的方式不同,但是最終都會歸結到通過查看GC引用鏈來確定內存泄漏的具體位置(原因)。接下來,我們就以Dominator Tree爲例來講解如何使用MAT來判定是否有內存泄漏以及泄漏的具體原因。Dominator Tree,即支配樹,點擊Dominator Tree選項如下圖所示,然後使用條件過濾(黃色方框輸入),找一個我們認爲可能發生了內存泄漏的類:
在這裏插入圖片描述
 從上圖可以看到,在Dominator Tree列出了很多SingleInstanceActivity的實例,而一般SingleInstanceActivity是不該有這麼多實例的,因此,基本可以斷定發生了內存泄漏,至於內存泄漏的具體原因,就需要查看GC引用鏈。但在查看之前,我們需要理解下紅色方框幾個標籤的意義。

  • Shallow Heap

 對象自身佔用的內存大小,不包括引用的對象。如果是數組類型的對象,它的大小由數組元素的類型和長度決定;如果是非數組類型的對象,它的大小由其成員變量的數量和類型決定。

  • Retained Heap

 Retained Heap就是當前對象被GC後,從Heap上總共能釋放掉多大的內存空間,這部分內存空間被稱之爲Retained Set。Retained Set指的是這個對象本身和它持有引用的對象以及這些引用對象的Retained Set所佔內存大小的總和。下面我們從一顆引用樹(GC引用鏈)來理解下Retained Set:
在這裏插入圖片描述
 假設A、B爲GC Root對象,根據Retained Set定義可知,對象E的Retained Set爲對象E、G,對象C的Retained Set爲對象C、D、E、F、G、H。另外,通過引用樹我們還可以演化得到本小節最重要的部分–支配樹(Dominator Tree),即在引用樹中如果一條到對象Y的路徑一定(必然)會經過對象X,那麼稱爲X支配Y,並且在所有支配Y的對象中,XY最近的一個對象就稱爲X直接支配Y,支配樹就是反應的這種直接支配關係。在支配樹中,父節點直接支配子節點。上圖就是引用樹轉換爲支配樹的例子,由此可以得到:

    • 對象C直接支配對象D、E、H,故C是D、E、H的父節點;
    • 對象D直接支配對象F,故D是F的父節點;
    • 對象E直接支配對象G,故E是G的父節點;

 通過支配樹,我們可以知道假如對象E被回收了,則會釋放E、G的內存,而不會釋放H的內存,因爲F可能還會引用着H,只有C被回收了,H的內存纔會被釋放。因此,我們可以得到一個結論通過MAT的Dominator Tree,可以清晰地得到一個對象的直接支配的對象,如果直接支配對象中出現了不該有的對象,就說明發生了內存泄漏。示例如下:
在這裏插入圖片描述
 從上圖可知,被選中的SingleInstanceActivity對象的直接支配對象出現了不該有的CommonUtils對象,因爲SingleInstanceActivity是要被回收的。換句話說,CommonUtils持有SingleInstanceActivity對象的引用,導致SingleInstanceActivity對象無法被正常回收,從而導致了內存泄漏。

2.3 LeakCanary

 考慮到篇幅原因,請移步至《Android性能優化3:常見內存泄漏與優化(三)》。

 文章最後,我們藉助Dalvik 虛擬機和 Sun JVM 在架構和執行方面有什麼本質區別?一文中的一段話作個總結,個人覺得這對理解JVM/Dalvik/ART的本質比較有啓發意義,即JVM其核心目的,是爲了構建一個真正跨OS平臺,跨指令集的程序運行環境(VM)。DVM的目的是爲了將android OS的本地資源和環境,以一種統一的界面提供給應用程序開發。嚴格來說,DVM不是真正的VM,它只是開發的時候提供了VM的環境,並不是在運行的時候提供真正的VM容器。這也是爲什麼JVM必須設計成stack-based的原因。

參考文獻:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章