1. 程式人生 > >使用 Memory Profiler 檢視 Java 堆和記憶體分配

使用 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。

為什麼應分析您的應用記憶體

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

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

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

  • 在時間線中查詢可能會導致效能問題的不理想的記憶體分配模式。
  • 轉儲 Java 堆以檢視在任何給定時間哪些物件耗盡了使用記憶體。 長時間進行多個堆轉儲可幫助識別記憶體洩漏。
  • 記錄正常使用者互動和極端使用者互動期間的記憶體分配以準確識別您的程式碼在何處短時間分配了過多物件,或分配了洩漏的物件。

如需瞭解可減少應用記憶體使用的程式設計做法,請閱讀管理您的應用記憶體

Memory Profiler 概覽

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

圖 1. Memory Profiler

如圖 1 所示,Memory Profiler 的預設檢視包括以下各項:

  1. 用於強制執行垃圾回收 Event 的按鈕。
  2. 用於記錄記憶體分配情況的按鈕。 此按鈕僅在連線至執行 Android 7.1 或更低版本的裝置時才會顯示。
  3. 用於放大/縮小時間線的按鈕。
  4. 用於跳轉至實時記憶體資料的按鈕。
  5. Event 時間線,其顯示 Activity 狀態、使用者輸入 Event 和螢幕旋轉 Event。
  6. 記憶體使用量時間線,其包含以下內容:
    • 一個顯示每個記憶體類別使用多少記憶體的堆疊圖表,如左側的 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 所示。

注:如果您需要更精確地瞭解轉儲的建立時間,可以通過呼叫  在應用程式碼的關鍵點建立堆轉儲。

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

  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 狀態下反覆操作多次。 旋轉裝置經常會導致應用洩漏 、 或 View 物件,因為系統會重新建立 ,而如果您的應用在其他地方保持對這些物件之一的引用,系統將無法對其進行垃圾回收。
  • 處於不同的 Activity 狀態時,在您的應用與另一個應用之間切換(導航到主螢幕,然後返回到您的應用)。

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