Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
目錄
內存泄漏檢測實踐
翻譯:使用 Memory Profiler 查看 Java 堆和內存分配
為什么應分析您的應用內存
Memory Profiler 概覽
如何計算內存
查看內存分配
在分析時提高應用程序性能
查看全局JNI引用
捕獲堆轉儲【重要】
將堆轉儲另存為 HPROF
分析內存的技巧
MAT 工具詳解
獲取及打開 .hprof 文件
工具欄
主界面 Overview
default_report 窗口
兩個重要概念
Shallow heap:本身占用內存
Retained Heap:引用占用內存
Histogram 和 Dominator Tree
右鍵菜單:Query Browser
內存泄漏檢測實踐
1、給項目中集成LeakCanary
進行內存泄漏檢測,發現有內存泄漏后該工具會進行提示。
2、有內存泄露后我們使用AS的profiler
工具進行分析並獲取到.hprof
文件:
- 打開 profiler 工具,點擊進入 MEMORY 區域
- 為了模擬泄漏場景,我們對有內存泄漏的頁面進行反復操作,然后點擊圖中的垃圾桶進行
強制gc
,再點擊垃圾桶旁邊的按鈕獲取堆棧信息
- 當堆棧信息獲取完成后,會在下面彈出一個列表框,我們將排序方式選為
Arrange by package
,這樣好定位我們自己的代碼。找到我們的代碼后我們真的發現,應該已經被回收了的 Activity 還占用這內存,為什么它沒有被銷毀,還有那些對象引用着它呢?這時我們就需要點擊保存按鈕,導出.hprof
文件進行具體分析了
3、導出以后我們會得到一個.hprof
文件,但是這個不是 mat 工具用到的標准文件,我們需要使用 sdk 自帶的platform-tools/hprof-conv.exe
工具進行轉換,命令:hprof-conv -z 1.hprof 1_mat.hprof
4、下來我們就需要使用 mat 進行分析了
- 打開以后我們點擊 Histogram
- 進入 Histogram 頁面,選擇
Group By package
以定位到我們自己的包,然后在搜索框輸入我們想要找的類,然后右鍵選擇merge shortest paths to Gc roots
->exclude all phantom/weak/soft etc.references
選項 - 然后就得到了這個類的引用樹,從樹中我們分析出我們的 Activity 是被誰引用了。
可以看到我們的 Activity 是被一個叫 imageView 的對象引用導致無法被釋放的,實際上是因為我們的這個 imageView 是靜態導致的:
static ImageView imageView; //靜態View導致Activity內存泄漏
翻譯:使用 Memory Profiler 查看 Java 堆和內存分配
Memory Profiler 是 Android Profiler 中的一個組件,可幫助您識別導致應用卡頓、凍結甚至崩潰的內存泄漏和流失[memory leaks and memory churn]。它顯示一個應用內存使用量的實時圖表[It shows a realtime graph of your app's memory use],讓您可以捕獲堆轉儲[capture a heap dump]、強制執行垃圾回收[force garbage collections]以及跟蹤內存分配[track memory allocations]。
要打開 Memory Profiler,請按以下步驟操作:
- 點擊 View > Tool Windows > Android Profiler(也可以點擊工具欄中的 Android Profiler
)。
- 從 Android Profiler 工具欄中選擇您想要分析的
設備和應用進程
。 - 點擊 MEMORY 時間線中的任意位置可打開 Memory Profiler。
或者,您可以在命令行中使用 dumpsys 檢查您的應用內存,同時 查看 logcat 中的 GC Event。
為什么應分析您的應用內存
Android 提供一個 托管內存環境(managed memory environment) —當它確定您的應用不再使用某些對象時,垃圾回收器會將未使用的內存釋放回堆中。 雖然 Android 查找未使用內存的方式在不斷改進,但對於所有 Android 版本,系統都必須在某個時間點短暫地暫停您的代碼。 大多數情況下,這些暫停難以察覺。 不過,如果您的應用分配內存的速度比系統回收內存的速度快,則當收集器釋放足夠的內存以滿足您的分配需要時,您的應用可能會延遲。 此延遲可能會導致您的應用跳幀[skip frames],並使系統明顯變慢。
盡管您的應用不會表現出變慢,但如果存在內存泄漏,則即使應用在后台運行也會保留該內存。 此行為會強制執行不必要的垃圾回收 Event,因而拖慢系統的內存性能。 最后,系統被迫終止您的應用進程以回收內存。 然后,當用戶返回您的應用時,它必須完全重啟。
為幫助防止這些問題,您應使用 Memory Profiler 執行以下操作:
- 在時間線[timeline]中查找可能會導致性能問題的不理想的內存分配模式[undesirable memory allocation patterns]。
- 轉儲 Java 堆以查看在任何給定時間哪些對象耗盡了使用內存。 長時間進行多個堆轉儲可幫助識別內存泄漏。
- 記錄正常用戶交互和極端用戶交互期間的內存分配以准確識別您的代碼在何處短時間分配了過多對象,或分配了泄漏的對象[allocating objects that become leaked]。
如需了解可減少應用內存使用的編程做法,請閱讀 管理您的應用內存。
Memory Profiler 概覽
當您首次打開 Memory Profiler 時,您將看到一條表示應用內存使用量的詳細時間線,並可訪問用於強制執行垃圾回收、捕捉堆轉儲和記錄內存分配的各種工具。
圖 1. Memory Profiler
如圖 1 所示,Memory Profiler 的默認視圖包括以下各項:
- 用於強制執行垃圾回收 Event 的按鈕。
- 用於捕獲堆轉儲的按鈕。
- 用於記錄內存分配情況的按鈕。 此按鈕僅在連接至運行
Android 7.1
或更低版本的設備時才會顯示。 - 用於放大/縮小時間線的按鈕。
- 用於跳轉至實時內存數據的按鈕。
- Event 時間線,其顯示 Activity 狀態、用戶輸入 Event 和屏幕旋轉 Event。
- 內存使用量時間線,其包含以下內容:
- 一個顯示每個內存類別使用多少內存的堆疊圖表[stacked graph],如左側的 y 軸以及頂部的彩色鍵所示。
- 虛線表示
分配的對象數
,如右側的 y 軸所示。 - 用於表示每個垃圾回收 Event 的圖標。
不過,如果您使用的是運行 Android 7.1 或更低版本的設備,則默認情況下,並不是所有分析數據均可見。 如果您看到一條消息,其顯示“Advanced profiling is unavailable for the selected process”,則需要 啟用高級分析 以查看下列內容:
- Event 時間線
- 分配的對象數
- 垃圾回收 Event
在 Android 8.0 及更高版本上,始終為可調試應用啟用高級分析。
如何計算內存
您在 Memory Profiler(圖 2)頂部
看到的數字取決於您的應用根據 Android 系統機制所提交的所有私有內存頁面數[private memory pages]。 此計數不包含與系統或其他應用共享的頁面。
圖 2. Memory Profiler 頂部的內存計數圖例
內存計數中的類別如下所示:
- Java:從 Java 或 Kotlin 代碼分配的對象內存。
- Native:從 C 或 C++ 代碼分配的對象內存。 即使您的應用中不使用 C++,您也可能會看到此處使用的一些原生內存,因為 Android 框架使用原生內存代表您處理各種任務[handle various tasks on your behalf],如處理圖像資源和其他圖形時,即使您編寫的代碼采用 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 可為您顯示有關對象分配的以下信息:
- 分配哪些
類型
的對象以及它們使用多少
空間。 - 每個分配的堆疊追蹤[stack trace],包括在哪個
線程
中。 - 對象在何時_被取消分配_(僅當使用運行 Android 8.0 或更高版本的設備時)。
如果您的設備運行 Android 8.0 或更高版本,您可以隨時按照下述方法查看您的對象分配: 只需點擊並按住時間線,並拖動
選擇您想要查看分配的區域(如視頻 1 中所示)。 不需要開始記錄會話,因為 Android 8.0 及更高版本附帶設備內置分析工具,可持續跟蹤您的應用分配。
https://storage.googleapis.com/androiddevelopers/videos/studio/memory-profiler-allocations-jvmti.mp4
視頻 1. 對於Android 8.0 及更高版本,選擇一個現有時間線區域以查看對象分配
如果您的設備運行 Android 7.1 或更低版本,則在 Memory Profiler 工具欄中點擊 Record memory allocations 。 記錄時,Android Monitor 將跟蹤您的應用中進行的所有分配。 操作完成后,點擊 Stop recording
(同一個按鈕;請參閱視頻 2)以查看分配。
https://storage.googleapis.com/androiddevelopers/videos/studio/memory-profiler-allocations-record.mp4
視頻 2. 對於 Android 7.1 及更低版本,您必須顯式記錄內存分配
在選擇一個時間線區域后(或當您使用運行 Android 7.1 或更低版本的設備完成記錄會話時),已分配對象的列表將顯示在時間線下方,按類名稱[class name]
進行分組,並按其堆計數[heap count]
排序。
注:在 Android 7.1 及更低版本上,您最多可以記錄 65535 個分配。 如果您的記錄會話超出此限值,則記錄中僅保存最新的 65535 個分配。 (在 Android 8.0 及更高版本中,則沒有實際的限制。)
要檢查分配記錄,請按以下步驟操作:
- 瀏覽列表以查找堆計數異常大且可能存在泄漏的對象。 為幫助查找已知類,點擊 Class Name 列標題以按字母順序排序。 然后點擊一個類名稱。 此時在右側將出現 Instance View 窗格,顯示
該類的每個實例
,如圖 3 中所示。 - 在 Instance View 窗格中,點擊一個實例。 此時下方將出現 Call Stack 標簽,顯示該實例被分配到何處以及哪個線程中。
- 在 Call Stack 標簽中,點擊任意行以在編輯器中跳轉到該代碼。
圖 3. 有關每個已分配對象的詳情顯示在右側的 Instance View 中。
默認情況下,左側的分配列表按類名稱排列。 在列表頂部,您可以使用右側的下拉列表在以下排列方式之間進行切換:
- Arrange by class:基於類名稱對所有分配進行分組。
- Arrange by package:基於軟件包名稱對所有分配進行分組。
- Arrange by callstack:將所有分配分組到其對應的調用堆棧[Groups all allocations into their corresponding call stack]。
在分析時提高應用程序性能
為了在分析時提高應用程序性能,內存分析器默認情況下會定期對內存分配進行采樣[samples ]。 在運行API級別26或更高級別的設備上進行測試時,可以使用“Allocation Tracking”下拉列表更改此行為。
可用選項如下:
- Full:捕獲內存中的所有對象分配。 這是Android Studio 3.2及更早版本中的默認行為。 如果您有一個分配了大量對象的應用程序,您可能會在分析時觀察到應用程序的可見速度下降[observe visible slowdowns]。
- Sampled:定期在內存中采樣對象分配。 這是默認選項,在分析時對應用程序性能的影響較小。 在很短的時間內分配大量對象的應用程序仍然可能會出現明顯的減速。
- Off:停止跟蹤應用的內存分配。
注意:默認情況下,Android Studio會在執行CPU錄制時停止跟蹤實時分配,並在CPU錄制完成后重新打開。 您可以在CPU錄制配置對話框中更改此行為。
查看全局JNI引用
Java Native Interface(JNI)是一個允許Java代碼和 native code 相互調用的框架。
JNI引用由 native code 手動管理,因此 native code 使用的Java對象可能會保持活動太長時間。如果在沒有先明確刪除[first being explicitly deleted]的情況下丟棄JNI引用,Java堆上的某些對象可能無法訪問。此外,可能耗盡[exhaust]全局JNI引用限制。
要解決此類問題,請使用Memory Profiler中的 JNI heap 視圖瀏覽所有全局JNI引用,並按Java類型和本機調用堆棧對其進行過濾。通過此信息,您可以找到創建和刪除全局JNI引用的時間和位置。
在您的應用程序運行時,選擇要檢查的時間軸的一部分,然后從 class list 上方的下拉菜單中選擇JNI堆。然后,您可以像往常一樣檢查堆中的對象,然后雙擊 Allocation Call Stack 選項卡中的對象,以查看在代碼中分配和釋放JNI引用的位置,如圖4所示。
要檢查應用程序的JNI代碼的內存分配,您必須將應用程序部署到運行Android 8.0或更高版本的設備。
有關JNI的更多信息,請參閱 JNI tips。
捕獲堆轉儲【重要】
堆轉儲顯示在您捕獲堆轉儲時您的應用中哪些對象正在使用內存
。 特別是在長時間的用戶會話后,堆轉儲會顯示您認為不應再位於內存中卻仍在內存中的對象,從而幫助識別內存泄漏。 在捕獲堆轉儲后,您可以查看以下信息:
- 您的應用已分配哪些類型的對象,以及每個類型分配多少。
- 每個對象正在使用多少內存。
- 在代碼中的何處仍在引用每個對象。
- 對象所分配到的調用堆棧。(目前,如果您在記錄分配時捕獲堆轉儲,則只有在 Android 7.1 及更低版本中,堆轉儲才能使用調用堆棧。)
圖 4. 查看堆轉儲
要捕獲堆轉儲,在 Memory Profiler 工具欄中點擊 Dump Java heap 。
在轉儲堆期間,Java 內存量可能會暫時增加。 這很正常,因為堆轉儲與您的應用發生在同一進程中,並需要一些內存來收集數據。
堆轉儲顯示在內存時間線下,顯示堆中的所有類類型,如圖 5 所示。
注:如果您需要更精確地了解轉儲的創建時間,可以通過調用 [dumpHprofData()](https://developer.android.google.cn/reference/android/os/Debug.html#dumpHprofData(java.lang.String) 在應用代碼的關鍵點創建堆轉儲。
在類列表中,您可以查看以下信息:
- Allocations: 堆中分配數
- Native Size: 此對象類型使用的native內存總量。 此列僅適用於Android 7.0及更高版本。您將在這里看到一些用Java分配內存的對象,因為Android使用native內存來處理某些框架類,例如Bitmap。
- Shallow Size: 此對象類型使用的Java內存總量
- Retained Size: 因此類的所有實例而保留的內存總大小
您可以使用已分配對象列表上方的兩個菜單來選擇要檢查的堆轉儲以及如何組織數據。
從左側的菜單中,選擇要檢查的堆:
- Default heap:系統未指定堆時。
- App heap:您的應用在其中分配內存的主堆[primary heap]。
- Image heap:系統啟動映像[system boot image],包含啟動期間預加載[preloaded]的類。 此處的分配保證絕不會移動或消失。
- Zygote heap:copy-on-write heap,其中的應用進程是從 Android 系統中派生[forked]的。
從右側菜單中選擇如何排列分配:
- Arrange by class:基於類名稱對所有分配進行分組。
- Arrange by package:基於軟件包名稱對所有分配進行分組。
- Arrange by callstack:將所有分配分組到其對應的調用堆棧。此選項僅在記錄分配[recording allocations]期間捕獲堆轉儲[capture the heap dump]時才有效。即使如此,堆中的對象也很可能是在您開始記錄之前分配的,因此這些分配會首先顯示,且只按類名稱列出。
默認情況下,此列表按 Retained Size 列排序。 您可以點擊任意列標題以更改列表的排序方式。
在 Instance View 中,每個實例都包含以下信息:
- Depth:從任意 GC root 到所選實例的最短 hops 數。
- Native Size: native內存中此實例的大小。此列僅適用於Android 7.0及更高版本。
- Shallow Size:此實例Java內存的大小。
- Retained Size:此實例支配[dominator]的內存大小(根據 [dominator 樹](https://en.wikipedia.org/wiki/Dominator_(graph_theory))。
默認情況下,堆轉儲_不會_向您顯示每個已分配對象的堆疊追蹤。 要獲取堆疊追蹤,在點擊 Dump Java heap 之前,您必須先開始 記錄內存分配。 然后,您可以在 Instance View 中選擇一個實例,並查看 Call Stack 標簽以及 References 標簽,如圖 5 所示。不過,在您開始記錄分配之前,可能已分配一些對象,因此,調用堆棧不能用於這些對象。 包含調用堆棧的實例在圖標
上用一個“堆棧”標志表示。(遺憾的是,由於堆疊追蹤需要您執行分配記錄,因此,您目前無法在 Android 8.0 上查看堆轉儲的堆疊追蹤。)
圖 5. 捕獲堆轉儲需要的持續時間標示在時間線中
要檢查您的堆,請按以下步驟操作:
1、瀏覽列表以查找堆計數[heap counts]異常大且可能存在泄漏的對象。 為幫助查找已知類,點擊 Class Name 列標題以按字母順序排序。 然后點擊一個類名稱。 此時在右側將出現 Instance View 窗格,顯示該類的每個實例,如圖 5 中所示。
或者,您可以通過單擊 Filter 或按 Control + F 並在搜索字段中輸入
類名或包名
來快速定位對象。 也可以從下拉菜單中選擇 Arrange by callstack 來按方法名稱搜索。如果要使用正則表達式,請選中Regex旁邊的框。如果您的搜索查詢區分大小寫,請選中匹配大小寫旁邊的框。
2、在 Instance View 窗格中,點擊一個實例。此時下方將出現 References,顯示該對象的每個引用。或者,點擊實例名稱旁的箭頭
以查看其所有字段
,然后點擊一個字段名稱查看其所有引用
。 如果您要查看某個字段的實例詳情,右鍵點擊該字段並選擇 Go to Instance。
3、在 References 標簽中,如果您發現某個引用可能在泄漏內存,則右鍵點擊它並選擇 Go to Instance。 這將從堆轉儲中選擇對應的實例,顯示您自己的實例數據。
在您的堆轉儲中,請注意由下列任意情況引起的內存泄漏:
- 長時間引用
Activity
、Context
、View
、Drawable
和其他對象,可能會保持對Activity
或Context
容器的引用。 - 可以保持
Activity
實例的非靜態內部類,如Runnable
。 - 對象保持時間超出所需時間的緩存。
將堆轉儲另存為 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
導入堆轉儲文件
要導入HPROF(.hprof)文件,請單擊 Sessions 窗格中 Load from file,然后從文件瀏覽器中選擇該文件。
您還可以通過將 HPROF 文件從文件瀏覽器拖到編輯器窗口中來導入HPROF文件。
分析內存的技巧
使用 Memory Profiler 時,您應對應用代碼施加壓力[stress your app code]並嘗試強制內存泄漏
。在應用中引發內存泄漏的一種方式是,先讓其運行一段時間,然后再檢查堆。泄漏在堆中可能逐漸匯聚到分配頂部[Leaks might trickle up to the top of the allocations in the heap]。不過,泄漏越小,您越需要運行更長時間才能看到泄漏。
您還可以通過以下方式之一觸發內存泄漏:
- 將設備從縱向旋轉為橫向,然后在不同的 Activity 狀態下反復操作多次。 旋轉設備經常會導致應用泄漏
Activity、Context、View
對象,因為系統會重新創建 Activity,而如果您的應用在其他地方保持對這些對象之一的引用,系統將無法對其進行垃圾回收。 - 處於不同的 Activity 狀態時,在您的應用與另一個應用之間切換(導航到主屏幕,然后返回到您的應用)。
提示:您還可以使 用monkeyrunner 測試框架執行上述步驟。
MAT 工具詳解
MAT,Memory Analyzer Tool
,一個基於Eclipse的免費內存分析工具,是一個快速、功能豐富的 JAVA heap 分析工具,它可以幫助我們查找內存泄漏
和查看內存消耗
情況。使用內存分析工具從眾多的對象中進行分析,快速的計算出在內存中對象的占用大小,看看是誰阻止了垃圾收集器的回收工作,並可以通過報表
直觀的查看到可能造成這種結果的對象。
當然 MAT 也有獨立的不依賴 Eclipse 的版本,只不過這個版本在調試 Android 內存的時候,需要將 DDMS 生成的文件進行轉換,才可以在獨立版本的 MAT 上打開。不過 Android SDK 中已經提供了這個工具,所以使用起來也是很方便的。
MAT工具提供了三種選擇的方式:
- Update Site:在線安裝Eclipse插件的方式
- Archived Update Site:離線安裝Eclipse插件的方式
- Stand-alone Eclipse RCP Applications:獨立安裝方,1.8.0 獨立安裝版下載地址
獲取及打開 .hprof 文件
使用MAT既可以打開一個已有的堆快照,也可以通過MAT直接從活動Java程序中導出堆快照。
HPROF文件是MAT能識別的文件,HPROF文件存儲的是特定時間點,java進程的內存快照。有不同的格式來存儲這些數據,總的來說包含了快照被觸發時java對象和類在heap中的情況。由於快照只是一瞬間的事情,所以heap dump中無法包含一個對象在何時、何地(哪個方法中)被分配這樣的信息。
這個文件可以使用DDMS導出:DDMS中在Devices上面有一排按鈕,選擇一個進程后,點擊Dump HPROF file 按鈕,選擇存儲路徑保存后就可以得到對應進程的HPROF文件。eclipse插件可以把上面的工作一鍵完成。只需要點擊Dump HPROF file圖標,然后MAT插件就會自動轉換格式,並且在eclipse中打開分析結果。
在使用使用Eclipse或者AndroidStudio抓內存之前,一定要手動點擊 Initiate GC 按鈕手動觸發GC,這樣抓到的內存使用情況就是不包括Unreachable對象的。
Unreachable指的是可以被垃圾回收器回收的對象,但是由於沒有GC發生,所以沒有釋放,這時抓的內存使用中的Unreachable就是這些對象。
點擊Calculate Retained Size之后,會出現Retained Size這一列,可以看到Unreachable Object的對象其Retained Heap值都為0。
如果HPROF文件是通過AndroidStudio的profile工具導出的,由於這個不是 mat 工具用到的標准文件,我們需要使用 sdk 自帶的platform-tools/hprof-conv.exe
工具進行轉換,命令為:
hprof-conv -z 1.hprof 1_mat.hprof
轉換過后的.hprof
文件即可使用MAT工具打開了。
打開一個.hprof
文件時,首先會顯示Getting Start Wizard
向導彈框,默認會選擇了第一個,確定后會生成一個報告。這個無大礙。
注意:最好將
.hprof
文件放在一個單獨的文件夾內打開,因為你在操作過程中,會生成大量的臨時文件。
工具欄
- Overview:主界面
- Histogram:直方圖
- Dominator Tree:支配樹
- OQL:Object Query Language studio
- Thread OvewView:查看這個應用所有的Thread信息
- Run Expert System Test:運行專家系統測試
- Query Browser:查詢瀏覽器
- Find Object By Address
- Group:在Histogram和Domiantor Tree界面,可以選擇將結果用另一種Group的方式顯示(默認是Group by Object),切換到Group by package可以更好地查看具體是哪個包里的類占用內存大,也很容易定位到自己的應用程序。
- Calculate Retained Size:點擊后,會出現Retained Size這一列
主界面 Overview
我們需要關注的是下面Actions區域,介紹4種分析方法:
- Histogram: Lists number of instances per class 列出內存中的對象,對象的個數以及大小
- Dominator Tree: List the biggest objects and what they keep alive. 列出最大的對象以及其依賴存活的Object,大小是以Retained Heap為標准排序的
- Top Consumers: Print the most expensive objects grouped by class and by package. 通過圖形列出最大的object
- Duplicate Classes: Detect classes loaded by multiple class loaders. 通過MAT自動分析泄漏的原因
default_report 窗口
該窗口列出了可能有問題的代碼片段。點擊每個問題中的Details可以查看相關的詳情。
詳情頁面包含如下內容
- Description:問題簡要描述
- Shortest Paths To the Accumulation Point:在此列表中,我們可以追溯到問題代碼的類樹的結構,並找到自己代碼中的類。
- Accumulated Objects in Dominator Tree:在此列表中,我們可以看見創建的大量的對象
- Accumulated Objects by Class in Dominator Tree:在此列表中,我們能看見創建大量對象相關的類。
- All Accumulated Objects by Class:在此列表中,會按類別划分的所有累計對象。
兩個重要概念
Shallow heap:本身占用內存
Shallow size就是對象本身占用內存的大小,不包含其引用的對象。
- 常規對象(非數組)的Shallow size由其成員變量的數量和類型決定
- 數組類型的對象的shallow size由數組元素的類型(對象類型、基本類型)和數組長度決定
注意:因為不像c++的對象本身可以存放大量內存,java的對象成員都是些引用。真正的內存都在堆上,看起來是一堆原生的
byte[]
、char[]
、int[]
,所以我們如果只看對象本身的內存,那么數量都很小。所以我們看到 Histogram 圖是以Shallow size進行排序的,排在第一位的一般都是byte[]
。
Retained Heap:引用占用內存
Retained Heap的概念,它表示如果一個對象被釋放掉,那么該對象引用的所有對象,包括被遞歸引用的對象,被釋放的內存。
例如,如果一個對象的某個成員new了一大塊int數組,那這個int數組也可以計算到這個對象中。與shallow heap比較,Retained heap可以更精確的反映一個對象實際占用的大小,因為如果該對象釋放,retained heap都可以被釋放。
但是,Retained Heap並不總是那么有效。
例如,我在A里new了一塊內存,賦值給A的一個成員變量,同時我讓B也指向這塊內存。此時,因為A和B都引用到這塊內存,所以A釋放時,該內存不會被釋放。所以這塊內存不會被計算到A或者B的Retained Heap中。
為了糾正這點,MAT中的 Leading Object(例如A或者B)不一定只是一個對象,也可以是多個對象。此時,(A,B)這個組合的Retained Set就包含那塊大內存了。對應到MAT的UI中,在Histogram中,可以選擇 Group By class, superclass or package來選擇這個組。
為了計算Retained Memory,MAT引入了Dominator Tree。
例如,對象A引用B和C,B和C又都引用到D,計算Retained Memory時:
- A的包括A本身和B,C,D。
- B和C因為共同引用D,所以B,C 的Retained Memory都只是他們本身。
- D當然也只是自己。
在這里例子中,樹根是A,而B,C,D是他的三個兒子,B,C,D不再有相互關系。
我覺得是為了加快計算的速度,MAT將對象引用圖
轉換成對象引用樹
。把引用圖變成引用樹后,計算Retained Heap就會非常方便,顯示也非常方便。對應到 MAT UI 上,在 dominator tree 這個view中,顯示了每個對象的 shallow heap 和 retained heap。然后可以以該節點為樹根,一步步的細化看看 retained heap 到底是用在什么地方了。
這種從圖到樹的轉換確實方便了內存分析,但有時候會讓人有些疑惑。本來對象B是對象A的一個成員,但因為B還被C引用,所以B在樹中並不在A下面,而很可能是平級。
為了糾正這點,MAT中點擊右鍵,可以 List objects 中選擇 with outgoing references 和 with incoming references。這是個真正的引用圖的概念,
- outgoing references :表示該對象的出節點(被該對象引用的對象)
- incoming references :表示該對象的入節點(引用到該對象的對象)
為了更好地理解 Retained Heap,下面引用一個例子來說明:
把內存中的對象看成下圖中的節點,並且對象和對象之間互相引用。這里有一個特殊的節點GC Roots,這就是reference chain(引用鏈)的起點:
上圖中藍色節點代表僅僅只有通過obj1才能直接或間接訪問的對象。因為可以通過GC Roots訪問,所以上圖的obj3不是藍色節點。因此上圖中obj1的retained size是obj1、obj2、obj4的shallow size總和。
上圖obj1的retained size是obj1、obj2、obj3、obj4的shallow size總和。而obj2的retained size是obj3、obj4的shallow size總和。
Histogram 和 Dominator Tree
Histogram的主要作用是查看一個instance的數量,一般用來查看自己創建的類的實例的個數。
可以很容易的找出占用內存最多的幾個對象,根據百分比(Percentage)來排序。
可以分不同維度來查看對象的Dominator Tree視圖,Group by class、Group by class loader、Group by package
Dominator Tree和Histogram的區別是站的角度不一樣,Histogram是站在類
的角度上去看,Dominator Tree是站的對象實例
的角度上看,Dominator Tree可以更方便的看出其引用關系。
通過查看Object的個數,結合代碼就可以找出存在內存泄露的類(即可達但是無用的對象,或者是可以重用但是重新創建的對象)
Histogram中還可以對對象進行Group,更方便查看自己Package中的對象信息。
右鍵菜單:Query Browser
菜單名稱 | 子選項 | 描述 |
---|---|---|
List objects | With Outgoing References | 顯示選中對象持有哪些對象 |
With Incoming References | 顯示選中對象被哪些對象持有。如果一個類有很多不需要的實例,那么可以找到哪些對象持有該對象,讓這個對象沒法被回收。 | |
Show objects by class | With Outgoing References | 顯示選中對象持有哪些對象, 這些對象按類合並在一起排序 |
With Incoming References | 顯示選中對象被哪些對象持有,這些對象按類合並在一起排序 | |
Merge Shortest Paths to GC Roots. | 從GC Roots節點到該對象的最短引用路徑 | |
With all references | 顯示選中對象到GC根節點的引用路徑,包括所有類型引用。 | |
Exclude weak/soft/phantom references | 排除了弱/軟/虛引用 |
菜單名稱 | 子選項 | 描述 |
---|---|---|
Java Basics | References Statistics | 顯示引用和對象的統計信息 |
Class Loader Explorer | 列出類加載器,包括定義的類 | |
Customized Retained Set | 計算選中對象的保留堆,排除指定的引用 | |
Find Strings | ||
Group By Value | ||
Open in Dominator Tree | 對選中對象生成支配樹 | |
Show as Histogram | 展示任意對象的直方圖 | |
Thread Details | 顯示線程的詳細信息和屬性 | |
Thread Overview and Stacks | - |
菜單名稱 | 子選項 | 描述 |
---|---|---|
Java Collections | Array Fill Ratio | 輸出給定數組中,非基本類型、非null對象個數占比 |
Arrays Grouped by Size | 顯示數組的直方圖,按大小分組 | |
Collection Fill Ratio | 輸出給定集合中,非基本類型、非 null 對象個數占比 | |
Collections Grouped by Size | 顯示集合的直方圖,按大小分組 | |
Hash Entries | 展開顯示指定HashMap或Hashtable中的鍵值對 | |
Map Collision Ratio | 輸出指定的映射集合的碰撞率 | |
Primitive Arrays With a Constant Value | 列出基本數據類型的數組,這些數組是由一個常數填充的 | |
Leak Identification | Component Report | 展示組件報告,分析可能的內存浪費或者低效使用的組件 |
Top Consumers | 輸出內存浪費最大的那個組件 | |
Search Queries:搜索列出所有Queries選項的具體含義,包含搜索區、輸入關鍵字后匹配的Queries選項列表區,點擊選項后的具體含義解釋區。
可以看到,所有的命令其實就是配置不同的SQL查詢語句,比如我們最常用的:
List objects -> with incoming references
:查看這個對象持有的外部對象引用List objects -> with outcoming references
:查看這個對象被哪些外部對象引用Path To GC Roots -> exclude all phantim/weak/soft etc. references
:查看這個對象的GC Root,不包含虛、弱引用、軟引用,剩下的就是強引用。從GC上說,除了強引用外,其他的引用在JVM需要的情況下是都可以 被GC掉的,如果一個對象始終無法被GC,就是因為強引用的存在,從而導致在GC的過程中一直得不到回收,因此就內存溢出了。Merge Shortest path to GC root
:找到從GC根節點到一個對象或一組對象的共同路徑
2019-3-20