文章目錄
在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移動設備平臺的核心組成部分之一。類似於傳統的JVM,Dalvik虛擬機
是在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。
- (3) 運行時堆劃分不同
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 Space
、Allocation Space
、Image Space
以及Large Object Space
,其中,Zygote Space
和Allocation Space
和Dalvik的一樣,Image Space
用來存放一些預加載類,Large Object Space
用來分配一些大對象(默認大小爲12Kb)。需要注意的是,Zygote Space
和Image 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.cpp
的main函數
進入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_invocation
的Init()
函數,該函數源碼如下,位於源碼根目錄下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)。
- 每個垃圾回收時間的圖標。
- 一個堆疊圖表。顯示每個內存類別當前使用多少內存,如左側的y軸和頂部的彩色健所示。
2.1.1 Allocation Tracker
Allocation Tracker
,即跟蹤一段時間內存分配情況
,Memory Profiler能夠顯示內存中的每個Java對象和JNI引用時如何分配的。我們需要關注如下信息:
- 分配了哪些類型的對象,分配了多大的空間;
- 對象分配的棧調用,是在哪個線程中調用的;
- 對象的釋放時間(只針對8.0+);
如果是Android 8.0
以上的設備,支持隨時查看對象的分配情況,具體的步驟如下:在時間軸上拖動以選擇要查看的哪個區域(時間段)的內存分配情況,如下圖所示:
接下來,我們就以上一篇文章中所提及的單例模式引起的內存泄漏爲例,來檢查內存分配的記錄,排查可能存在內存泄漏的對象。具體的步驟如下:
(1) 瀏覽列表以查找堆計數異常大
且可能存在泄漏的對象,即大對象。爲了幫助查找已知類,可以點擊下圖中黃色方框
的選項選擇使用Arrange by class
或Arrange by Package
按類名或者包名進行分組,然後再紅色方框中的第一個選項就會列出Class Name
或Package Name
,我們可以直接去查找目標類,也可以點擊下圖中的Filter 圖標
快速查找某個類,比如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圖標 實現,獲得某一時刻的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圖標 生成,輸出的文件格式爲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
:檢測出被多個類加載器加載的類;
其中,分析內存泄漏最常用的就是Histogram
和Dominator 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
的對象中,X
是Y
最近的一個對象就稱爲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的原因。
參考文獻: