Android Studio 如何分析內(nèi)存活動
前面的小節(jié)我們學(xué)習(xí)了如何分析 CPU 活動。本小節(jié)學(xué)習(xí)如何分析內(nèi)存活動。
1. 什么是 Memory Profiler
1.1 Memory Profiler 概覽
Memory Profiler 是 Android Profiler 中的一個組件,可幫助我們識別可能會導(dǎo)致應(yīng)用卡頓、凍結(jié)甚至崩潰的內(nèi)存泄露和內(nèi)存抖動。它顯示一個應(yīng)用內(nèi)存使用量的實時圖表,讓我們可以捕獲堆轉(zhuǎn)儲、強制執(zhí)行垃圾回收以及跟蹤內(nèi)存分配。
如果我們的應(yīng)用分配內(nèi)存的速度比系統(tǒng)回收內(nèi)存的速度快,則當(dāng)回收器釋放足夠的內(nèi)存以滿足我們的分配需要時,我們的應(yīng)用可能會延遲。此延遲可能會導(dǎo)致我們的應(yīng)用跳幀,并使系統(tǒng)明顯變慢。如果存在內(nèi)存泄露,則即使應(yīng)用在后臺運行也會保留該內(nèi)存。此行為會強制執(zhí)行不必要的垃圾回收事件,因而拖慢系統(tǒng)其余部分的內(nèi)存性能。
為幫助防止這些問題,我們應(yīng)使用 Memory Profiler 執(zhí)行以下操作:
-
在時間軸上查找可能會導(dǎo)致性能問題的不理想的內(nèi)存分配模式;
-
轉(zhuǎn)儲 Java 堆以查看在任何給定時間哪些對象耗盡了內(nèi)存。在很長一段時間內(nèi)進行多次堆轉(zhuǎn)儲有助于識別內(nèi)存泄露;
-
記錄正常用戶交互和極端用戶交互期間的內(nèi)存分配,以準(zhǔn)確識別我們的代碼在何處短時間內(nèi)分配了過多對象,或分配了泄露的對象。
1.2 打開 Memory Profiler
要打開 Memory Profiler,請按以下步驟操作:
依次點擊 View > Tool Windows > Profiler,可以點擊工具欄中的 Profile 圖標(biāo)。
從 Android Profiler 工具欄中選擇要分析的設(shè)備和應(yīng)用進程。
點擊 MEMORY 時間軸上的任意位置以打開 Memory Profiler。
當(dāng)我們首次打開 Memory Profiler 時,我們將看到一條表示應(yīng)用內(nèi)存使用量的詳細時間軸,并可使用各種工具來強制執(zhí)行垃圾回收、捕獲堆轉(zhuǎn)儲以及記錄內(nèi)存分配。
-
用于強制執(zhí)行垃圾回收事件的按鈕;
-
用于捕獲堆轉(zhuǎn)儲的按鈕;
-
用于指定分析器多久捕獲一次內(nèi)存分配的下拉菜單。選擇適當(dāng)?shù)倪x項可幫助我們在分析時提高應(yīng)用性能;
-
用于縮放時間軸的按鈕;
-
用于跳轉(zhuǎn)到實時內(nèi)存數(shù)據(jù)的按鈕;
-
事件時間軸,顯示活動狀態(tài)、用戶輸入事件和屏幕旋轉(zhuǎn)事件;
-
內(nèi)存使用量時間軸,它會顯示以下內(nèi)容:
-
一個堆疊圖表,顯示每個內(nèi)存類別當(dāng)前使用多少內(nèi)存,如左側(cè)的 y 軸以及頂部的彩色鍵所示;
-
一條虛線,表示分配的對象數(shù),如右側(cè)的 y 軸所示;
-
每個垃圾回收事件的圖標(biāo)。
-
2. 如何計算內(nèi)存
我們在 Memory Profiler 頂部看到的數(shù)字基于我們的應(yīng)用根據(jù) Android 系統(tǒng)機制所提交的所有私有內(nèi)存頁面。此計數(shù)不包含與系統(tǒng)或其他應(yīng)用共享的頁面。
內(nèi)存計數(shù)中的類別如下:
-
Java:從 Java 或 Kotlin 代碼分配的對象的內(nèi)存;
-
Native:從 C 或 C++ 代碼分配的對象的內(nèi)存;
-
Graphics:圖形緩沖區(qū)隊列向屏幕顯示像素(包括 GL 表面、GL 紋理等等)所使用的內(nèi)存;(請注意,這是與 CPU 共享的內(nèi)存,不是 GPU 專用內(nèi)存。)
-
Stack:我們的應(yīng)用中的原生堆棧和 Java 堆棧使用的內(nèi)存。通常與我們的應(yīng)用運行多少線程有關(guān);
-
Code:我們的應(yīng)用用于處理代碼和資源(如 dex 字節(jié)碼、經(jīng)過優(yōu)化或編譯的 dex 代碼、.so 庫和字體)的內(nèi)存;
-
Others:我們的應(yīng)用使用的系統(tǒng)不確定如何分類的內(nèi)存;
-
Allocated:我們的應(yīng)用分配的 Java/Kotlin 對象數(shù)。此數(shù)字沒有計入 C 或 C++ 中分配的對象。
3. 內(nèi)存分配
3.1 如何查看內(nèi)存分配
內(nèi)存分配為我們顯示內(nèi)存中的每個 Java 對象和 JNI 引用是如何分配的。具體而言,Memory Profiler 可為我們顯示有關(guān)對象分配的以下信息:
-
分配了哪些類型的對象以及它們使用多少空間;
-
每個分配的堆棧軌跡,包括在哪個線程中;
-
對象在何時被取消分配。
如果我們的設(shè)備搭載的是 Android 8.0 或更高版本,我們可以隨時查看對象分配,具體操作步驟如下:在時間軸上拖動以選擇要查看哪個區(qū)域的分配。不需要開始記錄會話,因為 Android 8.0 及更高版本附帶設(shè)備內(nèi)置分析工具,可持續(xù)跟蹤我們的應(yīng)用分配。
如果我們的設(shè)備搭載的是 Android 7.1 或更低版本,請點擊 Memory Profiler 工具欄中的 Record memory allocations 圖標(biāo)。記錄時,Memory Profiler 會跟蹤我們的應(yīng)用中發(fā)生的所有分配。完成后,請點擊 Stop recording 圖標(biāo)以查看分配。
3.2 檢查分析分配記錄
選擇時間軸的某個區(qū)域后(或者使用搭載 Android 7.1 或更低版本的設(shè)備完成記錄會話后),已分配對象的列表將顯示在時間軸下方,按類名稱進行分組,并按其堆計數(shù)排序。
要檢查分配記錄,請按以下步驟操作:
-
瀏覽列表以查找堆計數(shù)異常大且可能存在泄露的對象。為幫助查找已知類,點擊 Class Name 列標(biāo)題以按字母順序排序。然后,點擊一個類名稱。此時右側(cè)將出現(xiàn) Instance View 窗格,顯示該類的每個實例;
-
在 Instance View 窗格中,點擊一個實例。此時下方將出現(xiàn) Call Stack 標(biāo)簽頁,顯示該實例被分配到何處以及在哪個線程中;
-
在 Call Stack 標(biāo)簽頁中,右鍵點擊任意行并選擇 Jump to Source,以在編輯器中打開該代碼。
我們可以使用已分配對象列表上方的兩個菜單來選擇要檢查的堆以及如何組織數(shù)據(jù)。從左側(cè)的菜單中,選擇要檢查的堆:
-
default heap:當(dāng)系統(tǒng)未指定堆時;
-
image heap:系統(tǒng)啟動映像,包含啟動期間預(yù)加載的類。此處的分配保證絕不會移動或消失;
-
zygote heap:寫時復(fù)制堆,其中的應(yīng)用進程是從 Android 系統(tǒng)中派生的;
-
app heap:我們的應(yīng)用在其中分配內(nèi)存的主堆;
-
JNI heap:顯示 Java 原生接口 (JNI) 引用被分配和釋放到什么位置的堆。
從右側(cè)的菜單中,選擇如何安排分配:
-
Arrange by class:根據(jù)類名稱對所有分配進行分組。這是默認選項;
-
Arrange by package:根據(jù)軟件包名稱對所有分配進行分組;
-
Arrange by callstack:將所有分配分組到其對應(yīng)的調(diào)用堆棧;
3.2 查看全局 JNI 引用
JNI 引用由原生代碼進行管理,因此原生代碼使用的 Java 對象可能會保持活動狀態(tài)太長時間。如果丟棄了 JNI 引用而未先明確將其刪除,Java 堆上的某些對象可能會變得無法訪問。此外,還可能會達到全局 JNI 引用限制。
要排查此類問題,請使用 Memory Profiler 中的 JNI heap 視圖來瀏覽所有全局 JNI 引用,并按 Java 類型和原生調(diào)用堆棧對其進行過濾。借助此信息,我們可以了解創(chuàng)建和刪除全局 JNI 引用的時間和位置。
在我們的應(yīng)用運行時,選擇我們要檢查的一部分時間軸,然后從類列表上方的下拉菜單中選擇 JNI heap。 我們隨后可以像往常一樣檢查堆中的對象,還可以雙擊 Allocation Call Stack 標(biāo)簽頁中的對象,以查看在代碼中將 JNI 引用分配和釋放到了什么位置,如下圖所示。
Tips:要檢查應(yīng)用的 JNI 代碼的內(nèi)存分配,必須將應(yīng)用部署到搭載 Android 8.0 或更高版本的設(shè)備上。
4. 堆轉(zhuǎn)儲
堆轉(zhuǎn)儲顯示在我們捕獲堆轉(zhuǎn)儲時我們的應(yīng)用中哪些對象正在使用內(nèi)存。特別是在長時間的用戶會話后,堆轉(zhuǎn)儲會顯示我們認為不應(yīng)再位于內(nèi)存中卻仍在內(nèi)存中的對象,從而幫助識別內(nèi)存泄露。
捕獲堆轉(zhuǎn)儲后,我們可以查看以下信息:
-
我們的應(yīng)用分配了哪些類型的對象,以及每種對象有多少;
-
每個對象當(dāng)前使用多少內(nèi)存;
-
在代碼中的什么位置保持著對每個對象的引用;
-
對象所分配到的調(diào)用堆棧。
4.1 如何捕獲堆轉(zhuǎn)儲
要捕獲堆轉(zhuǎn)儲,請點擊 Memory Profiler 工具欄中的 Dump Java heap 圖標(biāo)。 在轉(zhuǎn)儲堆期間,Java 內(nèi)存量可能會暫時增加。 這很正常,因為堆轉(zhuǎn)儲與我們的應(yīng)用發(fā)生在同一進程中,并需要一些內(nèi)存來收集數(shù)據(jù)。
堆轉(zhuǎn)儲出現(xiàn)在內(nèi)存時間軸下方,顯示堆中的所有類類型,如下圖所示。
在類列表中,我們可以查看以下信息:
-
Allocations:堆中的分配數(shù);
-
Native Size:此對象類型使用的原生內(nèi)存總量(以字節(jié)為單位)。只有在使用 Android 7.0 及更高版本時,才會看到此列;
-
Shallow Size:此對象類型使用的 Java 內(nèi)存總量(以字節(jié)為單位);
-
Retained Size:為此類的所有實例而保留的內(nèi)存總大?。ㄒ宰止?jié)為單位);
點擊一個類名稱可在右側(cè)打開 Instance View 窗口。列出的每個實例都包含以下信息:
-
Depth:從任意 GC 根到選定實例的最短跳數(shù);
-
Native Size:原生內(nèi)存中此實例的大小。 只有在使用 Android 7.0 及更高版本時,才會看到此列;
-
Shallow Size:Java 內(nèi)存中此實例的大??;
-
Retained Size:此實例所支配內(nèi)存的大小;
要檢查應(yīng)用的堆,請按以下步驟操作:
-
瀏覽列表以查找堆計數(shù)異常大且可能存在泄露的對象。為幫助查找已知類,點擊 Class Name 列標(biāo)題以按字母順序排序。然后,點擊一個類名稱。此時右側(cè)將出現(xiàn) Instance View 窗格,顯示該類的每個實例;
-
在 Instance View 窗格中,點擊一個實例。此時下方將出現(xiàn) References 標(biāo)簽頁,顯示對該對象的每個引用;
-
在 References 標(biāo)簽頁中,如果我們發(fā)現(xiàn)某個引用可能在泄露內(nèi)存,請右鍵點擊它并選擇 Go to Instance。這樣會從堆轉(zhuǎn)儲中選擇相應(yīng)的實例,從而向我們顯示它自己的實例數(shù)據(jù)。
4.2 HPROF 文件
捕獲堆轉(zhuǎn)儲后,只有在 Memory Profiler 正在運行時,才能在該分析器中查看數(shù)據(jù)。當(dāng)我們退出分析會話時,會丟失堆轉(zhuǎn)儲。因此,如果我們要保存堆轉(zhuǎn)儲以供日后查看,請將其導(dǎo)出到 HPROF 文件。
Sessions 窗格中每個 Heap Dump 條目的右側(cè)都有一個 Export Heap Dump 按鈕。在隨即顯示的 Export As 對話框中,使用 .hprof 文件擴展名保存文件。
要使用其他 HPROF 分析器(如 jhat),我們需要將 HPROF 文件從 Android 格式轉(zhuǎn)換為 Java SE HPROF 格式。 我們可以使用 android_sdk/platform-tools/ 目錄中提供的 hprof-conv 工具執(zhí)行此操作。運行包含兩個參數(shù)(即原始 HPROF 文件和轉(zhuǎn)換后 HPROF 文件的寫入位置)的 hprof-conv 命令。例如:
hprof-conv heap-original.hprof heap-converted.hprof
5. 小結(jié)
本節(jié)課程我們主要學(xué)習(xí)了如何分析內(nèi)存活動。本節(jié)課程的重點如下:
- 掌握如何查看內(nèi)存分配和堆;
- 掌握如何檢查分析內(nèi)存數(shù)據(jù)。