Android 內存剖析 – 發現潛在問題
簡介
移動平台上的開發和內存管理緊密相關。盡管隨著科技的進步,現今移動設備上的內存大小已經達到了低端桌麵設備的水平,但是現今開發的應用程序對內存的需求也在同步增長。主要問題出在設備的屏幕尺寸上-分辨率越高需要的內存越多。熟悉Android平台的開發人員一般都知道垃圾回收器並不能徹底杜絕內存泄露問題,對於大型應用而言,內存泄露對性能產生的影響是難以估量的,因此開發人員必須要有內存分析的能力。本文介紹一些有用的工具,以及如何使用它們來檢測這些關鍵的內存泄露問題。
工具
有很多工具可以用來幫助檢測內存泄露問題,這裏列舉了一些,並附上一點相應的介紹:
工具名稱: |
簡介: |
DDMS (Dalvik調試監視服務器) |
和Android一起推出的調試工具(android sdk的tools目錄下就有)。提供端口轉發服務、截屏、線程監控、堆dump、logcat、進程和無線狀態監控以及一些其他功能。可以通過”./ddms”命令啟動該工具,同時它也被集成在ADT中,安裝以後Eclipse切到DDMS視圖即可。 |
MAT (內存分析工具) |
快速的Java堆分析器,該工具可以檢測到內存泄露,降低內存消耗,它有著非常強大的解析堆內存空間dump能力。還有很多其他功能無法在這裏一一列出,可以安裝一下MAT的eclipse插件試試,要活的更多詳細信息請點擊這裏。 |
術語介紹
內存分析涉及到很多專用術語,他們在本文中將頻繁出現,這裏給出每個術語的定義:
術語: |
定義: |
堆大小 |
分配給Java堆的內存,在Android平台,這些內存都是針對每個Activity分配的(這還取決於設備) |
堆轉儲文件 |
一個包含了堆中信息的二進製文件 |
支配樹(Dominator Tree) |
一個用來圖形化展示對象之間關係的工具,詳情請參考wiki |
內存泄露 |
內存泄露是指有個引用指向一個不再被使用的對象,導致該對象不會被垃圾回收器回收。如果這個對象有個引用指向一個包括很多其他對象的集合,就會導致這些對象都不會被垃圾回收。因此,需要牢記,垃圾回收無法杜絕內存泄露。 |
GC根 |
GC根是指那些假設可達的對象。 通常包括所有當前棧和係統的類加載器加載的類中引用的對象。(【譯者注】棧裏引用的對象是指當前執行的方法裏的局部變量指向的對象,係統加載器加載的類引用的對象包括類的靜態屬性引用的對象) |
內存泄露
內存泄露是指有個引用指向一個不再被使用的對象,導致該對象不會被垃圾回收器回收。如果這個對象有個引用指向一個包括很多其他對象的集合,就會導致這些對象都不會被垃圾回收。

圖. 1
被一個對象直接消耗的內存
例子:
public final class myObject { // 頭部: 8字節
private int valueA; // 整型: 4字節
private int valueB; // 整型: 4字節
private char valueC[]; // 字符型數組: 4字節
}
一個myObject對象共消耗20個字節保留堆(Retained Heap)
因釋放了某個對象後,可以釋放的所有內存總和。
GC前:GC即將回收對象A
圖. 2
GC後:回收對象A(300字節)從而導致回收對象B(50字節)和C(50字節),同時釋放了對象B以後對象D也會被回收(100字節),因此回收對象A就可以釋放500字節的內存,所謂保留隊正式這些對象直接占用的淺堆總和。

圖 3
檢測內存泄露
Logcat輸出的log
第一種發現內存泄露的方法是檢查logcat輸出的log。當垃圾回收器工作時,可以在Logcat中看到它的消息,這消息長的樣子類似於:
D/dalvikm( 14302): GC_CONCURRENT freed 2349K, 65% free 3246K/9551K, external 4703K/5261K, paused 2ms+2ms
這條消息的第一個部分說明該消息產生的原因,一共有四種類型:
GC_CONCURRENT |
當堆變得很大,防止出現堆溢出異常時產生 |
GC_FOR_MALLOC |
如果GC_CONCURENT類型的操作沒有及時運行,並且應用程序還需要分配更多內存時產生。 |
GC_EXTERNAL_ALLOC |
在Android3.0 (Honeycomb)以前,釋放通過外部內存(externel memory, 通過JNI代碼中malloc分配得到的內存)時產生。Android3.0和更高版本中不再有這種類型的內存分配了。 |
GC_EXPLICIT |
調用System.gc時產生 |
“freed 2349K,” – 說明釋放了多少內存.
“65% free 3246K/9551K” – 65%表示目前可分配內存占比例,3426K表示當前活動對象所占內存,9551K表示堆大小。
“external 4703K/5261K” – indicates external memory allocation, how much external memory the app has allocated and the soft limit of allocation.說明外部內存的分配,已經分配了多少以及能夠分配的上限。
“paused 2ms+2ms” –說明GC運行完成需要的時間。
有了這些信息,我們就可以知道GC運行幾次以後有沒有成功釋放出一些內存,如果分配出去的內存在這幾個周期中持續增加,那麼很明顯存在內存泄露。下麵的例子中就是存在內存泄漏時的Log。(【譯者注】圖片有點不清楚,但是大概可以看出來GC運行了好多次,可分配內存比例反而從47%降到45%了)

圖. 4
OutOfMemoryError 異常
跟蹤內存分配情況
成功的發現內存泄露問題以後,就應該查找根源在哪裏了,有兩個工具可以用來輔助分析問題根源所在。
DDMS
DDMS是一個強大的工具,他可以提供有很價值的信息,它還可以生成一個HPROF dump文件,該文件可以使用MAT打開分析。在Eclipse中打開DDMS,隻需安裝ADT插件以後打開DDMS視圖即可。

圖. 5
圖. 6
圖. 7
內存分析工具 (MAT)
MAT是個強大的內存分析工具,可以單獨使用也可以作為Eclipse的插件(【譯者注】這個工具不在ADT中,可以在https://www.eclipse.org/mat/downloads.php下載,有stand-alone版本和Eclipse安裝的update URL),這兩種使用方法唯一的區別就是如何打開一個HPROF文件。獨立版本需要一個打包好的HPROF文件。我們可以使用android adk自帶的hprof-conv工具(在android sdk的tools目錄下)打包。如果使用Eclipse插件版的MAT則不需要,直接在Eclipse中打開MAT視圖即可。
概述
當打開HPROF文件後,可以看到一個Overview界麵,它由以下元素構成:
- Overview標簽頁,提供一個概覽界麵。
- Histogram視圖,它提供每個類的對象統計(本文稍後詳述)
- 支配樹(Dominator Tree),提供程序中最占內存的對象 (described later in the article).
- 對象查詢語言(Object Query Language Studio), 用來寫MAT查詢的工具.
-
專家係統測試(Expert System Test) –
- 堆Dump概況(Heap Dump Overview) –提供堆dump文件的詳細信息
- 疑似泄露點(Leak Suspects) – 提供內存泄露疑點占用內存大小,被誰加載的,以及類型等詳細信息。
- Top Components – 提供占內存最多的對象信息,還包括可能的內存浪費信息.
-
查詢瀏覽器(Query Browser) – 提供很多很有用的查詢,有助於內存分析,本文將會介紹最有用的那些查詢。根據地址查找對象 – 可以根據提供的一個地址查找某個特定的對象.
- 對象列表(List Objects) – 顯示應用中所有對象,以及這些對象持有哪些其他對象和被哪些其他對象持有,(MAT會提示查詢哪一個對象)。
- 根據類顯示對象(Show Objects by Class) – 列出每個類有多少對象.
- 到GC根節點的路徑(Path to GC Roots) – 顯示到根節點的引用路徑 (有好多過濾選項).
- 合並到GC根節點的最短路徑(Merge Shortest Paths to GC Roots) –找到從GC根節點到一個對象或一組對象的共同路徑。
- 即時支配(Immediate Dominators) – Finds and aggregates on a class level all objects dominating a given set of objects. 在給定的一組對象中,從類的層麵上查找並聚合所有支配關係。(【譯者注】好吧,我覺得實在有必要說一下支配的意思,支配在計算機的控製流理論中意思是假如說從起始節點到節點B的所有路徑都經過節點A,則節點A支配節點B。在垃圾回收理論中應該是指從某個對象在另外一個對象的保留堆中)
- 顯示保留集合(Show Retained Set) – 計算一個對象的保留堆大小.
- 餅圖 – 顯示持有內存最大的對象
- 直方圖 – 顯示每個類的對象數量
- 支配樹 – 列出所有對象,並按照對象持有的保留堆大小排序
- 檢查器 – 選擇一個對象,並顯示其詳細信息

圖. 8
直方圖(Histogram)
MAT最有用的工具之一,它可以列出任意一個類的實例數。查找內存泄露或者其他內存方麵問題是,首先看看最有可能出問題的類,這個類有多少個實例是個比較好的選擇。它支持使用正則表達式來查找某個特定的類,還可以計算出該類所有對象的保留堆最小值或者精確值。
-
計算保留堆大小
a) 計算保留堆最小值(Calculate Minimum Retained Size) –計算保留堆最小值,並顯示在表格中.
b) 計算保留堆精確值(Calculate Precise Retained Size) – 計算保留堆精確值(這個過程需要一點時間) 並且顯示在表格中. - 正則表達式(Regex pattern) – 讓用戶查詢某個特定的對象類

圖. 9
另外,當選擇了某條顯示條目後,可以通過右擊彈出菜單。在診斷內存相關問題時,這個菜單是個非常重要的工具。如果開發者懷疑這裏有個內存泄露,可以通過菜單直接查看該類的對象持有哪些其他對象,當然,MAT支持過濾查詢結果(比如說限製被持有對象的類型)。查詢結果出來時,列表通過另外一個有用的工具-”Path toGC Roots”-展示給開發人員。它支持多種過濾選項,比如說排除弱引用-這是最常見的一個選項,因為當GC運行時,被弱引用持有的對象會被GC直接回收,所以這種對象是不會造成內存泄露的,一般直接把這種信息排除。如果MAT預定義的查詢不能滿足用戶需求的話,它還支持自己定製查詢,定製的自由度非常大,擁有無限的可能。本文稍後會介紹如何高效的定製查詢。

圖. 10

圖 11
支配樹(Dominator Tree)
支配樹可以算是MAT中第二有用的工具,它可以將所有對象按照保留堆大小排序顯示。用戶可以直接在“Overview”選項頁中點擊“Dominator Tree”進入該工具,也可以在上麵提到的菜單中選擇“immediate dominators”進入該工具。前者顯示dump文件中所有的對象,後者會從類的層麵上查找並聚合所有支配關係。支配樹有以下重要屬性:
- 屬於X的子樹的對象表示X的保留對象集合。
- 如果X是Y的持有者,那麼X的持有者也是Y的持有者。
- 在支配樹中表示持有關係的邊並不是和代碼中對象之間的關係直接對應,比如代碼中X持有Y,Y持有Z,在支配樹中,X的子樹中會有Z。
這三個屬性對於理解支配樹而言非常重要,一個熟練的開發人員可以通過這個工具快速的找出持有對象中哪些是不需要的以及每個對象的保留堆。

圖. 12
查詢(Queries)
查詢是用來檢查對象樹的基本工具,內存分析就是在許多對象中查找不希望看到的引用關係的過程-這件事聽上去容易做起來難。如果可以過濾這些對象和應用關係的話可以使這項複雜的運動簡單不少。一個開發人員想要成功的調試內存問題,必須掌握兩個關鍵點。第一個是對自己的應用充分了解,如果對自己應用程序中的對象之間的關係不夠了解的話,是不能找到內存問題的。第二個是掌握過濾和查找的技巧。如果開發者知道對象結構,而且也可以快速的找到想要的東西,那麼找到那些異常狀況將會變得容易一些。這裏列出MAT工具所有內建的查詢:
(【譯者注】下麵表格中的前兩列都是MAT工具中菜單的名稱)
查詢: |
選項: |
描述: |
List objects |
With Outgoing References |
顯示選中對象持有哪些對象. |
With Incoming References |
顯示選中對象被哪些對象持有。[如果一個類有很多不需要的實例,那麼可以找到哪些對象持有該對象,讓這個對象沒法被回收] |
|
Show object by class |
With Outgoing References |
顯示選中對象持有哪些對象, 這些對象按類合並在一起排序 |
With Incoming References |
顯示選中對象被哪些對象持有.這些對象按類合並在一起排序 |
|
Path to GC Roots |
With all references |
顯示選中對象到GC根節點的引用路徑,包括所有類型引用. |
Exclude weak references |
顯示選中對象到GC根節點的引用路徑,排除了弱引用. [弱引用不會影響GC回收對象] |
|
Exclude soft references |
顯示選中對象到GC根節點的引用路徑,排除軟引用(【譯者注】軟引用持有的對象在內存空間足夠時,GC不回收,內存空間足夠時,GC回收) |
|
Exclude phantom references |
顯示選中對象到GC根節點的引用路徑,排除虛引用(【譯者注】虛引用是最弱的引用,get()總是返回null,當它的對象被GC回收時,GC將reference放在ReferenceQueue中,用戶代碼當發現這個reference在在ReferenceQueue時就知道它持有的對象已經被回收了,這時可以做一些清理工作。《Java編程思想》第四版,中文版,第87頁寫到Java的finilize方法是為了對象被回收前做清理工作,但是事實上會有隱患,虛引用正是彌補) |
|
Merge Shortest Paths to GC Roots. |
選項和“Path to GC Roots”一樣 |
顯示GC根節點到選中對象的引用路徑 |
Java Basics |
References Statistics Class Loader Explorer |
顯示引用和對象的統計信息,列出類加載器,包括定義的類 |
Customized Retained Set |
計算選中對象的保留堆,排除指定的引用 |
|
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 |
顯示集合的直方圖,按大小分組 |
|
Extract Hash Set Values |
列出指定hash集合中的元素 |
|
Extract List Values |
列出指定LinkedList,ArrayList或Vector中的元素 |
|
Hash Entries |
展開顯示指定HashMap或Hashtable中的鍵值對 |
|
Map Collision Ratio |
輸出指定的映射集合的碰撞率 |
|
Primitive Arrays With a Constant Value |
列出基本數據類型的數組,這些數組是由一個常數填充的。 |
|
Leak Identification |
Component Report Top Consumers |
分析可能的內存浪費或者低效使用的組件,並輸出最大的那個 |
報告(Reports)
MAT自帶有一個報告生成係統,他可以自動分析dump文件並且生成報告給用戶。第一種報告叫做“泄露疑點(Leak suspects)”,MAT分析dump文件,檢查是否存在一些個頭大的對象被一些引用持有保持活動狀態,需要注意的是,泄露疑點並不一定是真的內存泄露。第二種報告叫做“頂級組件(Top Components)“,它包括可能的內存浪費信息,占用內存大的對象以及引用的統計信息。此報告對內存優化有很大幫助。
泄露疑點報告
泄露疑點報告包括一些潛在的內存泄露信息,再次強調一下,在這裏列出的對象並不一定是真正的內存泄露,但它仍然是檢查內存泄露的一個絕佳起點。報告主要包括描述和到達聚點的最短路徑, 第三部分(每種類型積累的對象)主要是從第二部分衍生出來的(根據類型排序)。

圖 13
“到聚點的最短路徑” 非常有用,它可以讓開發人員快速發現到底是哪裏讓這些對象無法被GC回收。

圖. 14
使用MAT檢測內存泄露
本小節主要介紹如何使用MAT檢測內存泄露的實踐部分,因此將會提供一段會造成內存泄露的代碼作為例子。
會內存泄露的樣例代碼
第一個內存泄露例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class
MainActivity extends
Activity {
//靜態屬性持有非靜態內部類的實例--這麼做非常糟糕
static
MyLeakedClass leakInstance = null ;
@Override
public
void
onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
//
Static field initialization
if
(leakInstance == null )
leakInstance
= new
MyLeakedClass();
ImageView
mView = new
ImageView( this );
mView.setBackgroundResource(R.drawable.leak_background);
setContentView(mView);
}
/*
*非靜態內部類
*/
class
MyLeakedClass {
int
someInt;
}
}
|

圖. 17
從第一次旋轉開始,每次都存在3mb的差異。每次旋轉後,堆的大小都漲3mb。這是有什麼東西不對勁的第一個警告,然後LogCat沒有顯示任何釋放已分配內存的信號。這時就需要檢查應用程序內部內存的情況了,運行DDMS,獲取HPROF文件,然後用自帶的MAT打開。
主屏幕上的確顯示了有什麼東西導致內存泄露了-它大概長的和圖18差不多。

圖. 18
餅圖上顯示,有很大一部分內存被資源文件占用-這很正常,因為任何應用都有一個GUI,但是本例子隻有一個資源文件,因此問題應該應該隱藏在這裏。還有兩個FrameLayout實例(每個有3mb)需要檢查,開發者還可以沿著一些路徑還檢查內存泄露問題。
基於直方圖的檢查
圖. 19
有三個比較大的bitmap對象,這麼看來這個本例最壞可能有兩到三個內存泄露。這幾個對象的內存大小符合LogCat的輸出,讓我們在檢查一下他們到GC根節點的路徑(剔除所有弱應用)。第一個bitmap對象看上去沒什麼問題,因為它隻有一個應用指向自己,沒有被任何其他對象引用,而且它正在等著被垃圾回收器回收。

圖. 20
圖 21
看來問題就出在剩下的第三個bitmap上了。它到GC根節點隻有一條路徑,而且它是被“leakInstance”對象持有的,正是leakInstance對象阻止了該bitmap對象被回收。

圖. 22
同時,在路徑上還有一個MainActivity對象 – 看到MainActivity對象不奇怪,因為每次旋轉都會新創建一個Activity,讓我們看看到底發生了什麼。首先通過正則表達式過濾器在直方圖中找出MainActivity對象。

圖. 23
圖. 24
第一個MainActivity對象有一個引用指向context和ActivityThread,因此它看上去是現在正在顯示的Activity。

圖. 25
第二個對象隻有一個引用指向自己,它正等著被垃圾回收,到目前為止,一切看上去都正常的。

圖 26
現在再看第三個 – 就是它了!有個強引用指向leakInstance對象,就是它阻止了該對象被垃圾回收。

圖. 27
基於支配樹的檢查
開發者可以通過很多種方法找到內存泄露。本文隻能介紹其中幾種,第二個要介紹的是基於支配樹視圖的。打開HPROF文件的支配樹視圖,按照保留堆大小進行排序。正如預料的一樣,最上麵的是資源類對象,還有三個FrameLayout類的對象(每個3mb)以及一個Bitmap對象(1mb)。FrameLayout對象看上去嫌疑很大,因此我們首先檢查它們。因為支配樹已經列出了具體的對象,因此我們可以直接查看它們到GC根節點的路徑。

圖. 28
第一個對象就是問題所在!它到GC根節點的唯一路徑正是leakInstance對象,因此它是一個泄露。

圖. 29
第二個和第三個對象分別是當前正在顯示和正在等著垃圾回收的。

圖. 30
讓我們在看看那個bitmap對象,它也有可能是一個內存泄露。選擇android.graphic.Bitmap,選擇顯示到GC根節點的路徑,剔除所有弱引用。

圖. 31
bitmap類型有三個對象,每個對象到GC根節點的路徑都可以查看到,上麵說的情況再次重演,三個實例中的兩個很顯然沒問題,但是第三個對象指向leakInstance,它一直都是活動狀態,不會被回收。

圖. 32
可能還有上百條路徑可以順藤摸瓜找出最終的泄露點,應該選擇哪條路徑取決於不同的開發者了,不同的開發人員有對如何分析內存有著不同的見解。
第二個內存泄露例子
第二個內存泄露場景發生在application context上。它將application context傳遞給一個單例模式的類,並將其作為一個屬性保留下來。這個操作將會使得MainActivity無法被垃圾回收。將context作為靜態屬性保存也會導致同樣的結果,因此這種做法應該避免。為了避免重複羅嗦,這裏隻介紹一種查找內存泄露的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class
MainActivity2 extends
Activity {
SingletonClass
mSingletonClass = null ;
@Override
public
void
onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
mSingletonClass
= SingletonClass.getInstance( this );
}
}
class
SingletonClass {
private
Context mContext = null ;
private
static
SingletonClass mInstance;
private
SingletonClass(Context context) {
mContext
= context;
}
public
static
SingletonClass getInstance(Context context) {
if
(mInstance == null )
{
mInstance
= new
SingletonClass(context);
}
return
mInstance;
}
}
|
圖. 33
概覽界麵並沒有提供什麼重要信息,因此開發人員需要繼續自己的探索。這個例子中沒有bitmap和其他資源,但是直方圖顯示這裏有很多MainActivity對象 – 檢查檢查它們也許能得到更多更有價值的消息。

圖. 34
將手機旋轉3次,直方圖顯示這裏有4個MainActivity對象。嗯,是時候檢查是不是有哪個對象阻止它們被回收了。要做到這一點,首先列出所有有incomming refrence的對象。隻需要展開視圖就很容易發現第一個對象就是當前正在顯示的Activity(他包含指向ActivityThread的引用)。

圖. 35
繼續列出其他兩個對象的到GC根節點的路徑。其中一個隻有一個引用指向它自己,另外一個指向mInstance,該引用在SignletonClass中,還有一個應用指向當前顯示的Activity(從mSigletonClass)。這正是一個泄露。

圖. 36
很明顯可以看出context讓垃圾回收無法回收該對象。另外還有一個問題 – 每次創建一個Acitivity實例的時候,context都被傳遞給SingletonClass。這是個嚴重的問題,因為context引用指向一個不在需要的Activity,從而讓這個Activity保持活躍無法被回收。在比較大的項目中,這中情況可能會導致一些意象不到的行為,並且這種問題很難被檢查出來。
譯文鏈接: https://www.importnew.com/2433.html
最後更新:2017-04-03 12:53:47