Android 內存管理機制
Android 的內存管理機制可以簡單概括為:系統沒有為內存提供交換區,它使用 paging 與 memory-mapping(mmapping) 來管理內存。
對開發來說,上面的管理機制意味著:
- 徹底釋放內存資源的唯一方法是釋放對象的引用,使對象可以被 GC(garbage collector) 回收。
- 有一種例外情況:沒有任何修改的文件,比如代碼本身,映射進內存后,如果系統需要使用這部分內存,會將這部分內存頁移出。
什么是內存泄漏
上面第 2 點在開發應用時,并沒有實際意義。因此在開發應用時,正確使用內存先要保證釋放掉不需要的內存資源。如果對象不需要了,但是由于沒有釋放對它的引用, GC 無法回收相應的內存資源,這部分內存就無法被利用了。這種情況就是所謂的“內存泄漏”。
內存泄漏是資源泄漏的一種,是由于沒有正確管理內存分配而造成內存不再使用卻沒有得到釋放。
內存泄漏就是對內存資源的浪費,內存通常是珍稀資源。所以,內存泄漏的影響很壞!
如果應用存在內存泄漏,對用戶來說,應用會越用越慢,并且會出現閃退;對開發者而言,會收到很多應用不穩定的評價,大量內存溢出( OOM )的錯誤日志,緊接著就是產品,測試,領導甚至老板的圍攻。
如何清除內存泄漏
排查泄漏
癥狀
前面提到,如果存在內存泄漏,并且每次泄漏的內存很多,則應用在使用過程中會時不時出現閃退的現象。如果查看日志數據,會看到OutOfMemoryError
類型的錯誤:
如果每次溢出的內存不多,則應用偶爾會出現閃退的現象,甚至平常不會出現閃退現象。但統計系統也會存在一些 OutOfMemoryError
類型的錯誤。
這里需要提醒的是:OutOfMemoryError
錯誤打印的棧信息中出錯的位置很有可能不是問題的原因。因為由于泄漏導致內存不夠時,任何位置都可能引起 OutOfMemoryError
錯誤。所以不要過分關注引起 OutOfMemoryError
的位置。
確診
思路
試著找到導致泄漏的操作路徑,拼命重復這個操作路徑!
這里需要提醒的是:
- 不是任意一臺設備都可以復現所有泄漏,使用同款設備嘗試。
- 要記錄測試數據供后續分析:測試前記錄下應用所占用的內存大小
m0
,重復多次后再記錄下應用所占用的內存大小m1
以及重復次數n
;出現 OOM 時,或者將要出現 OOM 時抓取應用的 heap dump 數據( .hprof 文件)。 - 如果每次泄漏的內存很少,重復次數
n
就需要很大,此時可以借助monkey
測試腳本來完成。
如果上面的 m1
明顯大于 m0
或者直接出現 OOM 錯誤,則應用一定存在內存泄漏。
定位泄漏
確認存在內存泄漏后,接下來就要定位哪些對象被泄漏了。目前比較好用的是 Memory Analyzer (MAT) 這個工具。MAT 是一個 Java heap analyzer
,用來查找內存泄漏與優化內存。
相關概念
Heap Dump
在一個時間點,給一個 Java 進程的內存使用情況拍個照,就是一份 Heap Dump 數據。通常 heap dump 包含了快照觸發時, Java 虛擬機堆 java 對象和類的相關信息,如:
- All Objects
Class, fields, primitive values and references - All Classes
Classloader, name, super class, static fields - GC Roots
Objects defined to be reachable by the JVM - Thread Stacks and Local Variables
The call-stacks of threads at the moment of the snapshot, and per-frame information about local objects
需要注意的是:heap dump 數據并不包含對象分配信息,所以無法從中獲知誰創建了對象,在哪里創建的對象。
Shallow vs. Retained Heap
Shallow heap 是一個對象實際占用的內存大小。
Retained set of X 指的是這樣的對象集合: X 對象被 GC 回收后,所有能被回收的對象集合。
Retained heap of X 指的是 retained set 中所有對象 shallow heap 的總和。
換一種說法: shallow heap 是一個對象在堆中占用的大小,retained heap 是對象被 GC 回收后,能釋放的堆大小。
Dominator Tree
dominator tree 是 MAT 提供的一種對象圖。將對象的引用關系圖轉成 dominator tree 可以使我們容易看清堆中內存的分布以及相關依賴。
下面是一些定義:
- 對象 x dominates 對象 y 則在對象圖中每一條從起點(或者根節點)到對象 y 的路徑必須經過對象 x 。
- 對象 y 的 immediate dominator x 是距離 y 最近的那個 dominator 。
- dominator tree 基于對象圖構建。在 dominator tree 中,每一個對象都是其子對象的 immediate dominator 。因此,對象與對象之間的依賴關系很容易被識別。
dominator tree 有以下幾點重要特征:
- 對象 x 的子樹中的對象集合就是 x 的 retained set 。
- 如果對象 x 是 對象 y 的 immediate dominator ,則 x 的 immediate dominator 也 dominates y ,以此類推。
- The edges in the dominator tree do not directly correspond to object references from the object graph.
根據上面的概念,下圖左邊的 object graph 可以轉換為右邊的 dominator tree :
Garbage Collection Roots
GC root 是 heap 外面那個可以訪問的對象。下面是可能的 GC root :
- System Class
Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* . - JNI Local
Local variable in native code, such as user defined JNI code or JVM internal code. - JNI Global
Global variable in native code, such as user defined JNI code or JVM internal code. - Thread Block
Object referred to from a currently active thread block. - Thread
A started, but not stopped, thread. - Busy Monitor
Everything that has called wait() or notify() or that is synchronized. For example, by calling synchronized(Object) or by entering a synchronized method. Static method means class, non-static method means object. - Java Local
Local variable. For example, input parameters or locally created objects of methods that are still in the stack of a thread. - Native Stack
In or out parameters in native code, such as user defined JNI code or JVM internal code. This is often the case as many methods have native parts and the objects handled as method parameters become GC roots. For example, parameters used for file/network I/O methods or reflection. - Finalizable
An object which is in a queue awaiting its finalizer to be run. - Unfinalized
An object which has a finalize method, but has not been finalized and is not yet on the finalizer queue. - Unreachable
An object which is unreachable from any other root, but has been marked as a root by MAT to retain objects which otherwise would not be included in the analysis. - Java Stack Frame
A Java stack frame, holding local variables. Only generated when the dump is parsed with the preference set to treat Java stack frames as objects. - Unknown
An object of unknown root type. Some dumps, such as IBM Portable Heap Dump files, do not have root information. For these dumps the MAT parser marks objects which are have no inbound references or are unreachable from any other root as roots of this type. This ensures that MAT retains all the objects in the dump.
尋找被泄漏對象(病灶)
MAT 的功能很多很強大,用來分析內存泄漏的話,主要使用 Dominator Tree
與 Histogram
這兩個功能。
MAT 相關功能簡介
使用 MAT 打開前面拿到的 hprof 文件:
首先看到的是 Overview
頁面。
里面 Details
部分顯示了堆的一些基本信息,以及 Unreachable Objects Histogram
入口,其中列出了堆中所有 Unreferenced 對象。
在 Actions
部分,有Histogram
和 Dominator Tree
的入口,前者更關注堆中對象的個數,后者更關注堆中對象的類型。其中列出的對象都是 Referenced 對象。被泄漏的對象一定是從里面找。
尋找被泄漏對象,可以從兩個方向下手:
方式一、從對象個數入手
如果前面的重復次數 n
已知的話,可以先從對象個數入手。重復一次泄漏路徑,就會泄漏一次對象,所以重復 n
次,泄漏的對象個數應該為 n
個。
打開 Histogram :
Histogram
頁面是一張表,表里的每一行是一個 java 類。第一列是類名,第二列是該類實例的個數,第三列是該類所有實例的 shallow heap
,第四列是該類所有實例的 retained heap
。
表的第一行可以輸入相應字段的條件過濾要顯示的結果,如排查應用層的泄漏,可以通過提供類名的關鍵詞過濾,使之只顯示相關類的信息。
前面重復次數 n
為 9 。排查對象個數為 9 附近的類,
不難發現 HomeTabActivity
這個類依然在 heap 中(App 此時已經不在前臺,且已經強制 GC)。因此,可以確認 HomeTabActivity
對象被泄漏了。
方式二、從對象類型開始
如果重復次數 n
不確定,則可以從 Dominator Tree
開始查。通過 Dominator Tree
,我們可以很方便的看到有哪些無法被 GC 回收的內存塊兒,以及對應內存塊兒的 GC root 。因此,我們可以通過排查并確認內存塊兒以及相應 GC root 是否合理來判斷此內存塊兒中的對象是否是被泄漏的對象。
打開 Dominator Tree
:
Dominator Tree
頁面也是一張表,表里的每一行是一個對象,第一列顯示了該對象的類名以及內存地址等,第二列顯示了該對象的 shallow heap
,第三列顯示了該對象的 retained heap
,第四列顯示了該對象的占比。
與 Histogram
類似,可以通過過濾縮小排查范圍,基于前面的分析,這次我們用更小的范圍排查。
需要注意,在 Class Name
這一列中,靠左邊一排圖標中,有些圖標左下角有小圓點,有些沒有。帶小圓點的對象就是前面提到的 GC root 。最右邊的字段,如: System Class
是 GC root 的類型。GC root 本身不會是泄漏的對象。
只需要排查不是 GC root 的那些對象。不難發現 heap 中存在 9 個 HomeTabActivity
類型的對象,與當時應用已經不在前臺的事實有出入,所以,這 9 個對象不應該存在,是被泄漏的,同時與之前重復次數 n
一致。
修復泄漏(治病)
找到被泄漏的對象后,接著要算出從該對象到 GC roots 的最短強引用路徑,找到本不該存在的路徑,對照相應源碼,修復掉錯誤的代碼邏輯,也就剔除了這個內存泄漏。
找病根
在 MAT 中如何看到一個對象到 GC root 的最短強引用路徑呢?
- 在
Histogram
中查看
在被泄漏類上面,點擊右鍵菜單中的 Merge Shartest Paths to GC Roots
--> exclude weak references
。就會看到這個 java 類中所有無法被釋放的對象的 GC roots ,點開每條路徑,可以看到引用關系。
從上圖,可以看到被泄漏的 HomeTabActivity
對象都是同一個 GC root 。
- 在
Dominator Tree
中查看
在被泄漏對象上面,通過右鍵菜單,選擇 Path to GC Roots
--> exclude weak references
可以看到該對象到 GC root 的一條路徑。
與通過 Dominator Tree
找到的路徑一致。
前面說這條路徑是不應該存在的,但是是什么原因導致其出現呢?接下來我們分析泄漏原因。
我們看到位于地址 0x4068aca8
的一個 HomeTabActivity
對象被位于地址 0x409cbbc8
的一個 Toast
對象通過成員變量 mContext
引用。接著,mContext
又被 Toast
中的內部類 TN
對象所引用,這個對象又是一個 Native Stack
類型的 GC root 。
根據上面的引用路徑,結合應用相關源碼:
HomeTabActivity
源代碼(部分);
/* HomeTabActivity.java */
public class HomeTabActivity extentds ... {
...
@Override
public void onBackPressed() {
...
if ((currentTime - touchTime) >= waitTime) {
Toast.makeText(this, "再按一次退出應用", Toast.LENGTH_SHORT).show();
} else {
...
}
...
}
...
}
發現在連按兩次返回鍵退出應用的功能代碼中,將 HomeTabActivity
對象的引用傳入 Toast.makeText()
。
因此泄漏的原因是:
HomeTabActivity
對象被生命周期更長的 Toast$TN
對象所引用,導致其實際生命周期超出了所預期的生命周期。
其實,站在 coder 的角度,內存泄漏本質就是該死不死,不論是什么具體形式導致了這種局面。
處方
本例中的泄漏是由于使用了不恰當的 Context
對象所致。
Android 中存在 Application Context 與 Activity Context 兩種具體的 Context
實例。前者的生命周期與應用進程的生命周期一樣,比后者長。
因此,在使用 Toast 時,應該使用 Application Context ,就不會出現該死不死的對象,也就不存在內存泄漏。修復代碼如下:
public class HomeTabActivity extentds ... {
...
@Override
public void onBackPressed() {
...
if ((currentTime - touchTime) >= waitTime) {
Toast.makeText(getApplicationContext(), "再按一次退出應用", Toast.LENGTH_SHORT).show();
} else {
...
}
...
}
...
}
附錄:
Android 常見內存泄漏形式
- Activity 泄漏 - 內部類
- Activity 泄漏 - 容器對象泄漏
- Activity 泄漏 - Static, Singleton
- 謹慎選擇合適的 Context
- 注意有生命周期對象的注銷
- 注意大胖子(Bitmap, WebView, Cursor)的及時回收