簡介 php
移動平臺上的開發和內存管理緊密相關。儘管隨着科技的進步,現今移動設備上的內存大小已經達到了低端桌面設備的水平,可是現今開發的應用程序對內存的需求也在同步增加。主要問題出在設備的屏幕尺寸上-分辨率越高須要的內存越多。熟悉Android平臺的開發人員通常都知道垃圾回收器並不能完全杜絕內存泄露問題,對於大型應用而言,內存泄露對性能產生的影響是難以估量的,所以開發人員必需要有內存分析的能力。本文介紹一些有用的工具,以及如何使用它們來檢測這些關鍵的內存泄露問題。 html
有不少工具能夠用來幫助檢測內存泄露問題,這裏列舉了一些,並附上一點相應的介紹: android
工具名稱: 正則表達式 |
簡介: 編程 |
DDMS (Dalvik調試監視服務器) 數組 |
和Android一塊兒推出的調試工具(android sdk的tools目錄下就有)。提供端口轉發服務、截屏、線程監控、堆dump、logcat、進程和無線狀態監控以及一些其餘功能。能夠經過」./ddms」命令啓動該工具,同時它也被集成在ADT中,安裝之後Eclipse切到DDMS視圖便可。 瀏覽器 |
MAT (內存分析工具) 服務器 |
快速的Java堆分析器,該工具能夠檢測到內存泄露,下降內存消耗,它有着很是強大的解析堆內存空間dump能力。還有不少其餘功能沒法在這裏一一列出,能夠安裝一下MAT的eclipse插件試試,要活的更多詳細信息請點擊這裏。 app |
內存分析涉及到不少專用術語,他們在本文中將頻繁出現,這裏給出每一個術語的定義: dom
術語: |
定義: |
堆大小 |
分配給Java堆的內存,在Android平臺,這些內存都是針對每一個Activity分配的(這還取決於設備) |
堆轉儲文件 |
一個包含了堆中信息的二進制文件 |
支配樹(Dominator Tree) |
一個用來圖形化展現對象之間關係的工具,詳情請參考wiki |
內存泄露 |
內存泄露是指有個引用指向一個再也不被使用的對象,致使該對象不會被垃圾回收器回收。若是這個對象有個引用指向一個包括不少其餘對象的集合,就會致使這些對象都不會被垃圾回收。所以,須要牢記,垃圾回收沒法杜絕內存泄露。 |
GC根 |
GC根是指那些假設可達的對象。 一般包括全部當前棧和系統的類加載器加載的類中引用的對象。(【譯者注】棧裏引用的對象是指當前執行的方法裏的局部變量指向的對象,系統加載器加載的類引用的對象包括類的靜態屬性引用的對象) |
內存泄露是指有個引用指向一個再也不被使用的對象,致使該對象不會被垃圾回收器回收。若是這個對象有個引用指向一個包括不少其餘對象的集合,就會致使這些對象都不會被垃圾回收。
圖. 1
圖. 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中,能夠在http://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界面,它由如下元素構成:
圖. 8
直方圖(Histogram)
MAT最有用的工具之一,它能夠列出任意一個類的實例數。查找內存泄露或者其餘內存方面問題是,首先看看最有可能出問題的類,這個類有多少個實例是個比較好的選擇。它支持使用正則表達式來查找某個特定的類,還能夠計算出該類全部對象的保留堆最小值或者精確值。
圖. 9
另外,當選擇了某條顯示條目後,能夠經過右擊彈出菜單。在診斷內存相關問題時,這個菜單是個很是重要的工具。若是開發者懷疑這裏有個內存泄露,能夠經過菜單直接查看該類的對象持有哪些其餘對象,固然,MAT支持過濾查詢結果(好比說限制被持有對象的類型)。查詢結果出來時,列表經過另一個有用的工具-」Path toGC Roots」-展現給開發人員。它支持多種過濾選項,好比說排除弱引用-這是最多見的一個選項,由於當GC運行時,被弱引用持有的對象會被GC直接回收,因此這種對象是不會形成內存泄露的,通常直接把這種信息排除。若是MAT預約義的查詢不能知足用戶需求的話,它還支持本身定製查詢,定製的自由度很是大,擁有無限的可能。本文稍後會介紹如何高效的定製查詢。
圖. 10
圖 11
支配樹(Dominator Tree)
支配樹能夠算是MAT中第二有用的工具,它能夠將全部對象按照保留堆大小排序顯示。用戶能夠直接在「Overview」選項頁中點擊「Dominator Tree」進入該工具,也能夠在上面提到的菜單中選擇「immediate dominators」進入該工具。前者顯示dump文件中全部的對象,後者會從類的層面上查找並聚合全部支配關係。支配樹有如下重要屬性:
這三個屬性對於理解支配樹而言很是重要,一個熟練的開發人員能夠經過這個工具快速的找出持有對象中哪些是不須要的以及每一個對象的保留堆。
圖. 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
|
publicclassMainActivityextendsActivity {
//靜態屬性持有非靜態內部類的實例--這麼作很是糟糕
staticMyLeakedClass leakInstance =null;
@Override
publicvoidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Static field initialization
if(leakInstance ==null)
leakInstance =newMyLeakedClass();
ImageView mView =newImageView(this);
mView.setBackgroundResource(R.drawable.leak_background);
setContentView(mView);
}
/*
*非靜態內部類
*/
classMyLeakedClass {
intsomeInt;
}
}
|
圖. 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
|
publicclassMainActivity2extendsActivity {
SingletonClass mSingletonClass =null;
@Override
publicvoidonCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSingletonClass = SingletonClass.getInstance(this);
}
}
classSingletonClass {
privateContext mContext =null;
privatestaticSingletonClass mInstance;
privateSingletonClass(Context context) {
mContext = context;
}
publicstaticSingletonClass getInstance(Context context) {
if(mInstance ==null) {
mInstance =newSingletonClass(context);
}
returnmInstance;
}
}
|
圖. 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保持活躍沒法被回收。在比較大的項目中,這中狀況可能會致使一些意象不到的行爲,而且這種問題很難被檢查出來。