android 如何分析應用的內存(十七)——使用MAT查看Android堆
前一篇文章,介紹了使用Android profiler中的memory profiler來查看Android的堆情況。
如Android 堆中有哪些對象,這些對象的引用情況是什么樣子的。
可是我們依然面臨一個比較嚴峻的挑戰:不管是app開發者,還是內存分析者而言,堆中的對象,非常之多,不僅有Android 原生的類,還有第三方庫使用的類。這些類在使用過程中,也可能因為有較大的shallow size 或者retained size而混淆內存的分析。
為了解決這樣的問題,我們更希望,通過不同時間點的堆,進行差分比較。即在時刻t1生成的堆heap1和t2時刻生成的堆heap2進行相互比較。
為此,我們將介紹java開發中重要的內存分析工具MAT。
MAT使用前的準備
在上一篇文章中,我們使用AS捕獲了堆,現在我們需要將其導出,用在MAT工具上。如下圖:
接下來將Android保存的heap dump進行格式轉換,以滿足MAT的需求。轉換格式的工具在Android SDK中。如下:
/Users/biaowan/Library/Android/sdk/platform-tools/hprof-conv ./mat/memory-test_malloc_int\[\].hprof mat_test1.hprof
MAT的使用
打開MAT之后,進入菜單欄->File->Open File。然后選擇剛才轉換之后的mat_test.hprof文件。如下圖
可見,主界面顯示一個overview的界面,該界面用英文詳細表述了具體細節,不在贅述。下面解釋上圖的八個標記。
-
標記1:打開overview 界面。即上面的主界面。
-
標記2:打開當前堆中的對象分布情況,默認按照類名進行排序。右鍵可以進行相應操作,各個操作什么意思,已經標明。如下圖
-
標記3:顯示本heap中的所有dominator tree(注意:dominator tree,已經在上一篇文章中介紹:android 如何分析應用的內存(十六)——使用AS查看Android堆:http://t.csdn.cn/GTWpR). 而各個對象應該怎么查看其對應的dominator tree。見標記2對應的右鍵說明。
-
標記4:Open Object Qurey Language,類似于使用SQL語句進行查詢。因為MAT提供的菜單功能已經完全夠Android使用,因此本文不再介紹
-
標記5:展示線程的名字,堆棧,本地變量等。但是Android 沒有提供這個功能,因此無法使用
-
標記6:打印各種報告,如下圖標記
-
標記7:即為標記2中,右鍵支持的各種操作,詳見標記2
-
標記8:搜索按鈕,可以按照地址進行搜索
為了能夠詳細說明,如何操作MAT,下文將會以各種問題作為模板,詳細介紹操作過程
問題1:如何查看某個類有哪些對象
- 點擊標記1,打開所有類的列表。
- 在第一行,鍵入需要查找的類,或者按照不同的大小進行排序和過濾
- 選中類,然后點擊右鍵,選擇List Objects.然后按照需求列出各個對象
問題2:如何查看某個對象到GC root的引用鏈
- 選中某個對象,右鍵選中Paths to GC root
- 再次選中exclude all xxx references
可見整個引用鏈清晰明了,不在繪圖說明
問題3:如何查看對象的Dominator tree(支配樹)
- 選中對象,右鍵選擇Java Basics
- 再次選中Open in Dominator tree
- 在彈出的框中,選擇finish。默認以對象排序
從圖中可以看到TaskRunable對象直接支配兩個對象,一個int數組,一個弱引用
問題4:如何查看一個對象的直接支配者
- 選中對象,然后右鍵,選擇immediate dominator
- 在彈出框中,選擇finish
可以看到我們選中的TaskRunable對象的直接支配者是一個Task對象
問題5:如何查看類加載器,是否重復加載同一個類
- 點擊標記1,打開overview界面
- 滾動界面到最底下
- 選擇Duplicate classes
問題6:如何查看堆中,最占內存的部分
- 點擊標記1,打開overview界面
- 滾動至最底下
- 選擇Top cosumer
從圖中可以看到,分別按照對象,類,類加載器,包名列出了最占內存的部分
問題7:如何查看堆的報告
- 點擊標記6.選擇Heap dump overview
- 在對報告中,點擊table of content 查看內容表(該字段,在報告的底部)
從中也可以直接查看最占內存的對象
問題8:如何進行泄露檢查
- 點擊標記6,選擇Leak Suspect
從圖中可以看到,有三個懷疑的對象,往下滾動,可以看到三個懷疑對象的詳細細節,如下
圖中,簡要說明了Task類,有2100個實例,占了29.51%的內容。點擊details它會顯示相應的引用鏈路徑。可清晰看到GC root的整個引用鏈。
多個Heap進行差分分析,查找內存問題
為了一步步演練,如何使用多個heap進行差分分析,我們選擇上一篇文章方案2中的例子:android 如何分析應用的內存(十六)——使用AS查看Android堆:http://t.csdn.cn/JYGFC。然后在同一個進程的兩個不同時刻,分別選取不同的heap,分別叫as_heap1.hprof和as_heap2.hprof.
場景1:MAT 自動分析兩個堆之間的內存泄露
-
按照上文提及的hprof-conv工具,將as_heap1.hprof,as_heap2.hprof分別轉換為mat_heap1.hprof,mat_heap2.hprof。然后用mat工具將其打開。
-
打開第二個heap的overvie操作欄,即標記1。滾動到最底部
-
選擇Leak suspects by Snapshot comparision.
-
在彈出的框中,選擇mat_heap1.hprof。然后點擊finish。讓mat_heap2.hprof與mat_heap1.hprof做差分分析,然后給出一個報告,如下(需要等待一段時間)
從圖中可以看到,懷疑com.example.test_malloc.Task對象泄露,它有4900個對象,占整個堆的49.26%。點擊details可以看到這個引用鏈,如下圖
從圖中可以看到,Task被DeviceManager的listener所持有,導致GC無法回收。所以找到了內存泄露點。
場景2:無法自動分析時,手動分析
當兩個heap堆,間隔時間較短,泄露的對象,占據整個堆的空間較小,此時mat無法進行自動分析。此時我們可以手動分析。
接下來,我們用時間間隔較小的兩個堆,分別叫mat_heap3.hprof和mat_heap4.hprof
注意:mat_heap3.hprof和mat_heap4.hprof是用AS 重新抓取的時間間隔較近的兩個堆
-
用mat打開mat_heap3.hprof,和mat_heap4.hprof
-
按照問題6,輸出消耗內存最大的部分。下面是mat_heap3.hprof的報告。
從中,我們看到,占據內存最大的首要對象是int數組。接下來我們手動分析兩個堆中的int數組之間的差距——即mat_heap4.hprof比mat_heap3.hprof多了哪幾個int數組
-
點擊mat_heap3.hprof的統計信息,即標記2.然后選中int[]。右鍵列出所有的對象。如下圖
-
點擊操作歷史記錄欄,右鍵list_objects… 然后點擊add to compare basket。如下圖
-
因為我們需要比較兩個heap堆的int[]情況,因此選中mat_heap4.hprof之后,按照步驟3,4做同樣操作。將會在compare basket窗口兩個需要比較的對象。然后點擊感嘆號,開始比較即可。如下:
對測試結果,進行簡單排序,shallow_heap #1 升序排列。即可展示heap3中沒有而heap4中具有的對象。這也是從抓取heap3時刻,到抓取heap4時刻之間,堆中多出來的int數組對象。為前排的10個對象。
按照前文的問題2即可查看其引用鏈,從而分析被誰持有,為何沒有被釋放掉。
在第2步中,輸出的top comsumer除了int數組以外,還有其他的對象,因此按照步驟3,4,5即可進行兩個堆的比較。我們已經以int[]為例子,做了詳細說明,就不再一一比較。
除了使用top comsumer輔助定位需要比較的對象以外,還可以對任何懷疑的對象進行比較。步驟完全相同。
至此,MAT的使用介紹完畢。
MAT彌補了AS在內存分析上的如下不足:
- 無法自定義Retained Set(這對于大型應用很有用)
- 無法進行地址查找
- 無法進行堆之間的比較
- 無法按照需要進行排序
- 無法按照需要進行過濾等
雖然MAT已經足夠強大,但是依然還有一個內存問題,懸而未決——怎么才能知道這些內存泄露是由哪一個線程觸發,它們又有怎樣的調用棧?
在多線程編程中,對象的泄露,即可能是對象之間的引用不合理,也可能是線程之間的邏輯不合理,如生產線程和消費線程不夠合理等等。MAT無法解決Android線程所帶來的內存泄露。
接下里,請期待如何用工具找到這種多線程帶來的內存泄露。