Android 性能優化 - 使用 Memory Profiler 查看 Java 堆和內存分配

轉載地址:https://developer.android.google.cn/studio/profile/memory-profiler

 

使用 Memory Profiler 查看 Java 堆和內存分配

Memory Profiler 是 Android Profiler 中的一個組件,可幫助您識別導致應用卡頓、凍結甚至崩潰的內存泄漏和流失。 它顯示一個應用內存使用量的實時圖表,讓您可以捕獲堆轉儲、強制執行垃圾回收以及跟蹤內存分配。

要打開 Memory Profiler,請按以下步驟操作:

  1. 點擊 View > Tool Windows > Android Profiler(也可以點擊工具欄中的 Android Profiler )。
  2. 從 Android Profiler 工具欄中選擇您想要分析的設備和應用進程。 如果您通過 USB 連接了某個設備但該設備未在設備列表中列出,請確保您已啓用 USB 調試
  3. 點擊 **MEMORY **時間線中的任意位置可打開 Memory Profiler。

或者,您可以在命令行中使用 dumpsys 檢查您的應用內存,同時查看 logcat 中的 GC Event

爲什麼應分析您的應用內存

Android 提供一個託管內存環境—當它確定您的應用不再使用某些對象時,垃圾回收器會將未使用的內存釋放回堆中。 雖然 Android 查找未使用內存的方式在不斷改進,但對於所有 Android 版本,系統都必須在某個時間點短暫地暫停您的代碼。 大多數情況下,這些暫停難以察覺。 不過,如果您的應用分配內存的速度比系統回收內存的速度快,則當收集器釋放足夠的內存以滿足您的分配需要時,您的應用可能會延遲。 此延遲可能會導致您的應用跳幀,並使系統明顯變慢。

儘管您的應用不會表現出變慢,但如果存在內存泄漏,則即使應用在後臺運行也會保留該內存。 此行爲會強制執行不必要的垃圾回收 Event,因而拖慢系統的內存性能。 最後,系統被迫終止您的應用進程以回收內存。 然後,當用戶返回您的應用時,它必須完全重啓。

爲幫助防止這些問題,您應使用 Memory Profiler 執行以下操作:

  • 在時間線中查找可能會導致性能問題的不理想的內存分配模式。
  • 轉儲 Java 堆以查看在任何給定時間哪些對象耗盡了使用內存。 長時間進行多個堆轉儲可幫助識別內存泄漏。
  • 記錄正常用戶交互和極端用戶交互期間的內存分配以準確識別您的代碼在何處短時間分配了過多對象,或分配了泄漏的對象。

如需瞭解可減少應用內存使用的編程做法,請閱讀管理您的應用內存

Memory Profiler 概覽

當您首次打開 Memory Profiler 時,您將看到一條表示應用內存使用量的詳細時間線,並可訪問用於強制執行垃圾回收、捕捉堆轉儲和記錄內存分配的各種工具。

圖 1. Memory Profiler

 

如圖 1 所示,Memory Profiler 的默認視圖包括以下各項:

  1. 用於強制執行垃圾回收 Event 的按鈕。
  2. 用於捕獲堆轉儲的按鈕
  3. 用於記錄內存分配情況的按鈕。 此按鈕僅在連接至運行 Android 7.1 或更低版本的設備時纔會顯示。
  4. 用於放大/縮小時間線的按鈕。
  5. 用於跳轉至實時內存數據的按鈕。
  6. Event 時間線,其顯示 Activity 狀態、用戶輸入 Event 和屏幕旋轉 Event。
  7. 內存使用量時間線,其包含以下內容:
    • 一個顯示每個內存類別使用多少內存的堆疊圖表,如左側的 y 軸以及頂部的彩色鍵所示。
    • 虛線表示分配的對象數,如右側的 y 軸所示。
    • 用於表示每個垃圾回收 Event 的圖標。

不過,如果您使用的是運行 Android 7.1 或更低版本的設備,則默認情況下,並不是所有分析數據均可見。 如果您看到一條消息,其顯示“Advanced profiling is unavailable for the selected process”,則需要啓用高級分析以查看下列內容:

  • Event 時間線
  • 分配的對象數
  • 垃圾回收 Event

在 Android 8.0 及更高版本上,始終爲可調試應用啓用高級分析。

如何計算內存

您在 Memory Profiler(圖 2)頂部看到的數字取決於您的應用根據 Android 系統機制所提交的所有私有內存頁面數。 此計數不包含與系統或其他應用共享的頁面。

圖 2. Memory Profiler 頂部的內存計數圖例

 

內存計數中的類別如下所示:

  • Java:從 Java 或 Kotlin 代碼分配的對象內存。
  • Native:從 C 或 C++ 代碼分配的對象內存。

    即使您的應用中不使用 C++,您也可能會看到此處使用的一些原生內存,因爲 Android 框架使用原生內存代表您處理各種任務,如處理圖像資源和其他圖形時,即使您編寫的代碼採用 Java 或 Kotlin 語言。

  • Graphics:圖形緩衝區隊列向屏幕顯示像素(包括 GL 表面、GL 紋理等等)所使用的內存。 (請注意,這是與 CPU 共享的內存,不是 GPU 專用內存。)

  • Stack: 您的應用中的原生堆棧和 Java 堆棧使用的內存。 這通常與您的應用運行多少線程有關。

  • Code:您的應用用於處理代碼和資源(如 dex 字節碼、已優化或已編譯的 dex 碼、.so 庫和字體)的內存。

  • Other:您的應用使用的系統不確定如何分類的內存。

  • Allocated:您的應用分配的 Java/Kotlin 對象數。 它沒有計入 C 或 C++ 中分配的對象。

    當連接至運行 Android 7.1 及更低版本的設備時,此分配僅在 Memory Profiler 連接至您運行的應用時纔開始計數。 因此,您開始分析之前分配的任何對象都不會被計入。 不過,Android 8.0 附帶一個設備內置分析工具,該工具可記錄所有分配,因此,在 Android 8.0 及更高版本上,此數字始終表示您的應用中待處理的 Java 對象總數。

與以前的 Android Monitor 工具中的內存計數相比,新的 Memory Profiler 以不同的方式記錄您的內存,因此,您的內存使用量現在看上去可能會更高些。 Memory Profiler 監控的類別更多,這會增加總的內存使用量,但如果您僅關心 Java 堆內存,則“Java”項的數字應與以前工具中的數值相似。

然而,Java 數字可能與您在 Android Monitor 中看到的數字並非完全相同,這是因爲應用的 Java 堆是從 Zygote 啓動的,而新數字則計入了爲它分配的所有物理內存頁面。 因此,它可以準確反映您的應用實際使用了多少物理內存。

注:目前,Memory Profiler 還會顯示應用中的一些誤報的原生內存使用量,而這些內存實際上是分析工具使用的。 對於大約 100000 個對象,最多會使報告的內存使用量增加 10MB。 在這些工具的未來版本中,這些數字將從您的數據中過濾掉。

查看內存分配

內存分配顯示內存中每個對象是如何分配的。 具體而言,Memory Profiler 可爲您顯示有關對象分配的以下信息:

  • 分配哪些類型的對象以及它們使用多少空間。
  • 每個分配的堆疊追蹤,包括在哪個線程中。
  • 對象在何時被取消分配(僅當使用運行 Android 8.0 或更高版本的設備時)。

如果您的設備運行 Android 8.0 或更高版本,您可以隨時按照下述方法查看您的對象分配: 只需點擊並按住時間線,並拖動選擇您想要查看分配的區域(如視頻 1 中所示)。 不需要開始記錄會話,因爲 Android 8.0 及更高版本附帶設備內置分析工具,可持續跟蹤您的應用分配。

 

視頻 1. 對於Android 8.0 及更高版本,選擇一個現有時間線區域以查看對象分配

如果您的設備運行 Android 7.1 或更低版本,則在 Memory Profiler 工具欄中點擊 Record memory allocations 。 記錄時,Android Monitor 將跟蹤您的應用中進行的所有分配。 操作完成後,點擊 Stop recording (同一個按鈕;請參閱視頻 2)以查看分配。

 

視頻 2. 對於 Android 7.1 及更低版本,您必須顯式記錄內存分配

在選擇一個時間線區域後(或當您使用運行 Android 7.1 或更低版本的設備完成記錄會話時),已分配對象的列表將顯示在時間線下方,按類名稱進行分組,並按其堆計數排序。

注:在 Android 7.1 及更低版本上,您最多可以記錄 65535 個分配。 如果您的記錄會話超出此限值,則記錄中僅保存最新的 65535 個分配。 (在 Android 8.0 及更高版本中,則沒有實際的限制。)

要檢查分配記錄,請按以下步驟操作:

  1. 瀏覽列表以查找堆計數異常大且可能存在泄漏的對象。 爲幫助查找已知類,點擊 Class Name 列標題以按字母順序排序。 然後點擊一個類名稱。 此時在右側將出現 Instance View 窗格,顯示該類的每個實例,如圖 3 中所示。
  2. 在 Instance View 窗格中,點擊一個實例。 此時下方將出現 Call Stack 標籤,顯示該實例被分配到何處以及哪個線程中。
  3. 在 Call Stack 標籤中,點擊任意行以在編輯器中跳轉到該代碼。

圖 3. 有關每個已分配對象的詳情顯示在右側的 Instance View 中。

 

默認情況下,左側的分配列表按類名稱排列。 在列表頂部,您可以使用右側的下拉列表在以下排列方式之間進行切換:

  • Arrange by class:基於類名稱對所有分配進行分組。
  • Arrange by package:基於軟件包名稱對所有分配進行分組。
  • Arrange by callstack:將所有分配分組到其對應的調用堆棧。

捕獲堆轉儲

堆轉儲顯示在您捕獲堆轉儲時您的應用中哪些對象正在使用內存。 特別是在長時間的用戶會話後,堆轉儲會顯示您認爲不應再位於內存中卻仍在內存中的對象,從而幫助識別內存泄漏。 在捕獲堆轉儲後,您可以查看以下信息:

  • 您的應用已分配哪些類型的對象,以及每個類型分配多少。
  • 每個對象正在使用多少內存。
  • 在代碼中的何處仍在引用每個對象。
  • 對象所分配到的調用堆棧。 (目前,如果您在記錄分配時捕獲堆轉儲,則只有在 Android 7.1 及更低版本中,堆轉儲才能使用調用堆棧。)

圖 4. 查看堆轉儲

 

要捕獲堆轉儲,在 Memory Profiler 工具欄中點擊 Dump Java heap  。 在轉儲堆期間,Java 內存量可能會暫時增加。 這很正常,因爲堆轉儲與您的應用發生在同一進程中,並需要一些內存來收集數據。

堆轉儲顯示在內存時間線下,顯示堆中的所有類類型,如圖 4 所示。

注:如果您需要更精確地瞭解轉儲的創建時間,可以通過調用 dumpHprofData() 在應用代碼的關鍵點創建堆轉儲。

要檢查您的堆,請按以下步驟操作:

  1. 瀏覽列表以查找堆計數異常大且可能存在泄漏的對象。 爲幫助查找已知類,點擊 Class Name 列標題以按字母順序排序。 然後點擊一個類名稱。 此時在右側將出現 Instance View 窗格,顯示該類的每個實例,如圖 5 中所示。
  2. 在 Instance View 窗格中,點擊一個實例。此時下方將出現 References,顯示該對象的每個引用。

    或者,點擊實例名稱旁的箭頭以查看其所有字段,然後點擊一個字段名稱查看其所有引用。 如果您要查看某個字段的實例詳情,右鍵點擊該字段並選擇 Go to Instance

  3. 在 References 標籤中,如果您發現某個引用可能在泄漏內存,則右鍵點擊它並選擇 Go to Instance。 這將從堆轉儲中選擇對應的實例,顯示您自己的實例數據。

默認情況下,堆轉儲不會向您顯示每個已分配對象的堆疊追蹤。 要獲取堆疊追蹤,在點擊 Dump Java heap 之前,您必須先開始記錄內存分配。 然後,您可以在 Instance View 中選擇一個實例,並查看 Call Stack 標籤以及 References 標籤,如圖 5 所示。不過,在您開始記錄分配之前,可能已分配一些對象,因此,調用堆棧不能用於這些對象。 包含調用堆棧的實例在圖標  上用一個“堆棧”標誌表示。 (遺憾的是,由於堆疊追蹤需要您執行分配記錄,因此,您目前無法在 Android 8.0 上查看堆轉儲的堆疊追蹤。)

在您的堆轉儲中,請注意由下列任意情況引起的內存泄漏:

  • 長時間引用 ActivityContextViewDrawable 和其他對象,可能會保持對 Activity 或 Context 容器的引用。
  • 可以保持 Activity 實例的非靜態內部類,如 Runnable
  • 對象保持時間超出所需時間的緩存。

圖 5. 捕獲堆轉儲需要的持續時間標示在時間線中

 

在類列表中,您可以查看以下信息:

  • Heap Count:堆中的實例數。
  • Shallow Size:此堆中所有實例的總大小(以字節爲單位)。
  • Retained Size:爲此類的所有實例而保留的內存總大小(以字節爲單位)。

在類列表頂部,您可以使用左側下拉列表在以下堆轉儲之間進行切換:

  • Default heap:系統未指定堆時。
  • App heap:您的應用在其中分配內存的主堆。
  • Image heap:系統啓動映像,包含啓動期間預加載的類。 此處的分配保證絕不會移動或消失。
  • Zygote heap:寫時複製堆,其中的應用進程是從 Android 系統中派生的。

默認情況下,此堆中的對象列表按類名稱排列。 您可以使用其他下拉列表在以下排列方式之間進行切換:

  • Arrange by class:基於類名稱對所有分配進行分組。
  • Arrange by package:基於軟件包名稱對所有分配進行分組。
  • Arrange by callstack:將所有分配分組到其對應的調用堆棧。 此選項僅在記錄分配期間捕獲堆轉儲時纔有效。 即使如此,堆中的對象也很可能是在您開始記錄之前分配的,因此這些分配會首先顯示,且只按類名稱列出。

默認情況下,此列表按 Retained Size 列排序。 您可以點擊任意列標題以更改列表的排序方式。

在 Instance View 中,每個實例都包含以下信息:

  • Depth:從任意 GC 根到所選實例的最短 hop 數。
  • Shallow Size:此實例的大小。
  • Retained Size:此實例支配的內存大小(根據 dominator 樹)。

將堆轉儲另存爲 HPROF

在捕獲堆轉儲後,僅當分析器運行時才能在 Memory Profiler 中查看數據。 當您退出分析會話時,您將丟失堆轉儲。 因此,如果您要保存堆轉儲以供日後查看,可通過點擊時間線下方工具欄中的 Export heap dump as HPROF file,將堆轉儲導出到一個 HPROF 文件中。 在顯示的對話框中,確保使用 .hprof 後綴保存文件。

然後,通過將此文件拖到一個空的編輯器窗口(或將其拖到文件標籤欄中),您可以在 Android Studio 中重新打開該文件。

要使用其他 HPROF 分析器(如 jhat),您需要將 HPROF 文件從 Android 格式轉換爲 Java SE HPROF 格式。 您可以使用 android_sdk/platform-tools/ 目錄中提供的 hprof-conv 工具執行此操作。 運行包括以下兩個參數的 hprof-conv命令:原始 HPROF 文件和轉換後 HPROF 文件的寫入位置。 例如:


 

hprof-conv heap-original.hprof heap-converted.hprof

分析內存的技巧

使用 Memory Profiler 時,您應對應用代碼施加壓力並嘗試強制內存泄漏。 在應用中引發內存泄漏的一種方式是,先讓其運行一段時間,然後再檢查堆。 泄漏在堆中可能逐漸匯聚到分配頂部。 不過,泄漏越小,您越需要運行更長時間的應用才能看到泄漏。

您還可以通過以下方式之一觸發內存泄漏:

  • 將設備從縱向旋轉爲橫向,然後在不同的 Activity 狀態下反覆操作多次。 旋轉設備經常會導致應用泄漏 ActivityContext 或 View 對象,因爲系統會重新創建 Activity,而如果您的應用在其他地方保持對這些對象之一的引用,系統將無法對其進行垃圾回收。
  • 處於不同的 Activity 狀態時,在您的應用與另一個應用之間切換(導航到主屏幕,然後返回到您的應用)。

 

提示: 您還可以使用 monkeyrunner 測試框架執行上述步驟。

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