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