Android 內(nèi)存管理機(jī)制
Android 的內(nèi)存管理機(jī)制可以簡單概括為:系統(tǒng)沒有為內(nèi)存提供交換區(qū)鹦筹,它使用 paging 與 memory-mapping(mmapping) 來管理內(nèi)存宋列。
對開發(fā)來說砰嘁,上面的管理機(jī)制意味著:
- 徹底釋放內(nèi)存資源的唯一方法是釋放對象的引用替久,使對象可以被 GC(garbage collector) 回收。
- 有一種例外情況:沒有任何修改的文件,比如代碼本身,映射進(jìn)內(nèi)存后涯保,如果系統(tǒng)需要使用這部分內(nèi)存,會將這部分內(nèi)存頁移出周伦。
什么是內(nèi)存泄漏
上面第 2 點(diǎn)在開發(fā)應(yīng)用時夕春,并沒有實(shí)際意義。因此在開發(fā)應(yīng)用時专挪,正確使用內(nèi)存先要保證釋放掉不需要的內(nèi)存資源及志。如果對象不需要了,但是由于沒有釋放對它的引用寨腔, GC 無法回收相應(yīng)的內(nèi)存資源速侈,這部分內(nèi)存就無法被利用了。這種情況就是所謂的“內(nèi)存泄漏”迫卢。
內(nèi)存泄漏是資源泄漏的一種倚搬,是由于沒有正確管理內(nèi)存分配而造成內(nèi)存不再使用卻沒有得到釋放。
內(nèi)存泄漏就是對內(nèi)存資源的浪費(fèi)乾蛤,內(nèi)存通常是珍稀資源每界。所以,內(nèi)存泄漏的影響很壞家卖!
如果應(yīng)用存在內(nèi)存泄漏眨层,對用戶來說,應(yīng)用會越用越慢篡九,并且會出現(xiàn)閃退谐岁;對開發(fā)者而言,會收到很多應(yīng)用不穩(wěn)定的評價榛臼,大量內(nèi)存溢出( OOM )的錯誤日志伊佃,緊接著就是產(chǎn)品,測試沛善,領(lǐng)導(dǎo)甚至老板的圍攻航揉。
如何清除內(nèi)存泄漏
排查泄漏
癥狀
前面提到,如果存在內(nèi)存泄漏金刁,并且每次泄漏的內(nèi)存很多帅涂,則應(yīng)用在使用過程中會時不時出現(xiàn)閃退的現(xiàn)象。如果查看日志數(shù)據(jù)尤蛮,會看到OutOfMemoryError
類型的錯誤:
如果每次溢出的內(nèi)存不多媳友,則應(yīng)用偶爾會出現(xiàn)閃退的現(xiàn)象,甚至平常不會出現(xiàn)閃退現(xiàn)象产捞。但統(tǒng)計(jì)系統(tǒng)也會存在一些 OutOfMemoryError
類型的錯誤醇锚。
這里需要提醒的是:OutOfMemoryError
錯誤打印的棧信息中出錯的位置很有可能不是問題的原因。因?yàn)橛捎谛孤?dǎo)致內(nèi)存不夠時,任何位置都可能引起 OutOfMemoryError
錯誤焊唬。所以不要過分關(guān)注引起 OutOfMemoryError
的位置恋昼。
確診
思路
試著找到導(dǎo)致泄漏的操作路徑,拼命重復(fù)這個操作路徑赶促!
這里需要提醒的是:
- 不是任意一臺設(shè)備都可以復(fù)現(xiàn)所有泄漏液肌,使用同款設(shè)備嘗試。
- 要記錄測試數(shù)據(jù)供后續(xù)分析:測試前記錄下應(yīng)用所占用的內(nèi)存大小
m0
鸥滨,重復(fù)多次后再記錄下應(yīng)用所占用的內(nèi)存大小m1
以及重復(fù)次數(shù)n
嗦哆;出現(xiàn) OOM 時,或者將要出現(xiàn) OOM 時抓取應(yīng)用的 heap dump 數(shù)據(jù)( .hprof 文件)婿滓。 - 如果每次泄漏的內(nèi)存很少吝秕,重復(fù)次數(shù)
n
就需要很大,此時可以借助monkey
測試腳本來完成空幻。
如果上面的 m1
明顯大于 m0
或者直接出現(xiàn) OOM 錯誤烁峭,則應(yīng)用一定存在內(nèi)存泄漏。
定位泄漏
確認(rèn)存在內(nèi)存泄漏后秕铛,接下來就要定位哪些對象被泄漏了约郁。目前比較好用的是 Memory Analyzer (MAT) 這個工具。MAT 是一個 Java heap analyzer
但两,用來查找內(nèi)存泄漏與優(yōu)化內(nèi)存鬓梅。
相關(guān)概念
Heap Dump
在一個時間點(diǎn),給一個 Java 進(jìn)程的內(nèi)存使用情況拍個照谨湘,就是一份 Heap Dump 數(shù)據(jù)绽快。通常 heap dump 包含了快照觸發(fā)時, Java 虛擬機(jī)堆 java 對象和類的相關(guān)信息紧阔,如:
- 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 數(shù)據(jù)并不包含對象分配信息坊罢,所以無法從中獲知誰創(chuàng)建了對象,在哪里創(chuàng)建的對象擅耽。
Shallow vs. Retained Heap
Shallow heap 是一個對象實(shí)際占用的內(nèi)存大小活孩。
Retained set of X 指的是這樣的對象集合: X 對象被 GC 回收后,所有能被回收的對象集合乖仇。
Retained heap of X 指的是 retained set 中所有對象 shallow heap 的總和憾儒。
換一種說法: shallow heap 是一個對象在堆中占用的大小,retained heap 是對象被 GC 回收后乃沙,能釋放的堆大小起趾。
Dominator Tree
dominator tree 是 MAT 提供的一種對象圖。將對象的引用關(guān)系圖轉(zhuǎn)成 dominator tree 可以使我們?nèi)菀卓辞宥阎袃?nèi)存的分布以及相關(guān)依賴警儒。
下面是一些定義:
- 對象 x dominates 對象 y 則在對象圖中每一條從起點(diǎn)(或者根節(jié)點(diǎn))到對象 y 的路徑必須經(jīng)過對象 x 训裆。
- 對象 y 的 immediate dominator x 是距離 y 最近的那個 dominator 。
- dominator tree 基于對象圖構(gòu)建。在 dominator tree 中缭保,每一個對象都是其子對象的 immediate dominator 。因此蝙茶,對象與對象之間的依賴關(guān)系很容易被識別艺骂。
dominator tree 有以下幾點(diǎn)重要特征:
- 對象 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.
根據(jù)上面的概念蹄衷,下圖左邊的 object graph 可以轉(zhuǎn)換為右邊的 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 的功能很多很強(qiáng)大,用來分析內(nèi)存泄漏的話愧口,主要使用 Dominator Tree
與 Histogram
這兩個功能睦番。
MAT 相關(guān)功能簡介
使用 MAT 打開前面拿到的 hprof 文件:
首先看到的是 Overview
頁面。
里面 Details
部分顯示了堆的一些基本信息耍属,以及 Unreachable Objects Histogram
入口托嚣,其中列出了堆中所有 Unreferenced 對象。
在 Actions
部分厚骗,有Histogram
和 Dominator Tree
的入口示启,前者更關(guān)注堆中對象的個數(shù),后者更關(guān)注堆中對象的類型领舰。其中列出的對象都是 Referenced 對象夫嗓。被泄漏的對象一定是從里面找。
尋找被泄漏對象冲秽,可以從兩個方向下手:
方式一舍咖、從對象個數(shù)入手
如果前面的重復(fù)次數(shù) n
已知的話,可以先從對象個數(shù)入手锉桑。重復(fù)一次泄漏路徑谎仲,就會泄漏一次對象,所以重復(fù) n
次刨仑,泄漏的對象個數(shù)應(yīng)該為 n
個郑诺。
打開 Histogram :
Histogram
頁面是一張表,表里的每一行是一個 java 類杉武。第一列是類名辙诞,第二列是該類實(shí)例的個數(shù),第三列是該類所有實(shí)例的 shallow heap
轻抱,第四列是該類所有實(shí)例的 retained heap
飞涂。
表的第一行可以輸入相應(yīng)字段的條件過濾要顯示的結(jié)果,如排查應(yīng)用層的泄漏,可以通過提供類名的關(guān)鍵詞過濾较店,使之只顯示相關(guān)類的信息士八。
前面重復(fù)次數(shù) n
為 9 。排查對象個數(shù)為 9 附近的類梁呈,
不難發(fā)現(xiàn) HomeTabActivity
這個類依然在 heap 中(App 此時已經(jīng)不在前臺婚度,且已經(jīng)強(qiáng)制 GC)。因此官卡,可以確認(rèn) HomeTabActivity
對象被泄漏了蝗茁。
方式二、從對象類型開始
如果重復(fù)次數(shù) n
不確定寻咒,則可以從 Dominator Tree
開始查哮翘。通過 Dominator Tree
,我們可以很方便的看到有哪些無法被 GC 回收的內(nèi)存塊兒毛秘,以及對應(yīng)內(nèi)存塊兒的 GC root 饭寺。因此,我們可以通過排查并確認(rèn)內(nèi)存塊兒以及相應(yīng) GC root 是否合理來判斷此內(nèi)存塊兒中的對象是否是被泄漏的對象叫挟。
打開 Dominator Tree
:
Dominator Tree
頁面也是一張表佩研,表里的每一行是一個對象,第一列顯示了該對象的類名以及內(nèi)存地址等霞揉,第二列顯示了該對象的 shallow heap
旬薯,第三列顯示了該對象的 retained heap
,第四列顯示了該對象的占比适秩。
與 Histogram
類似绊序,可以通過過濾縮小排查范圍,基于前面的分析秽荞,這次我們用更小的范圍排查骤公。
需要注意,在 Class Name
這一列中扬跋,靠左邊一排圖標(biāo)中阶捆,有些圖標(biāo)左下角有小圓點(diǎn),有些沒有钦听。帶小圓點(diǎn)的對象就是前面提到的 GC root 洒试。最右邊的字段,如: System Class
是 GC root 的類型朴上。GC root 本身不會是泄漏的對象垒棋。
只需要排查不是 GC root 的那些對象。不難發(fā)現(xiàn) heap 中存在 9 個 HomeTabActivity
類型的對象痪宰,與當(dāng)時應(yīng)用已經(jīng)不在前臺的事實(shí)有出入叼架,所以畔裕,這 9 個對象不應(yīng)該存在,是被泄漏的乖订,同時與之前重復(fù)次數(shù) n
一致扮饶。
修復(fù)泄漏(治病)
找到被泄漏的對象后乍构,接著要算出從該對象到 GC roots 的最短強(qiáng)引用路徑甜无,找到本不該存在的路徑,對照相應(yīng)源碼蜡吧,修復(fù)掉錯誤的代碼邏輯,也就剔除了這個內(nèi)存泄漏占键。
找病根
在 MAT 中如何看到一個對象到 GC root 的最短強(qiáng)引用路徑呢昔善?
- 在
Histogram
中查看
在被泄漏類上面,點(diǎn)擊右鍵菜單中的 Merge Shartest Paths to GC Roots
--> exclude weak references
畔乙。就會看到這個 java 類中所有無法被釋放的對象的 GC roots 君仆,點(diǎn)開每條路徑,可以看到引用關(guān)系牲距。
從上圖返咱,可以看到被泄漏的 HomeTabActivity
對象都是同一個 GC root 。
- 在
Dominator Tree
中查看
在被泄漏對象上面牍鞠,通過右鍵菜單咖摹,選擇 Path to GC Roots
--> exclude weak references
可以看到該對象到 GC root 的一條路徑。
與通過 Dominator Tree
找到的路徑一致难述。
前面說這條路徑是不應(yīng)該存在的萤晴,但是是什么原因?qū)е缕涑霈F(xiàn)呢?接下來我們分析泄漏原因胁后。
我們看到位于地址 0x4068aca8
的一個 HomeTabActivity
對象被位于地址 0x409cbbc8
的一個 Toast
對象通過成員變量 mContext
引用店读。接著,mContext
又被 Toast
中的內(nèi)部類 TN
對象所引用攀芯,這個對象又是一個 Native Stack
類型的 GC root 屯断。
根據(jù)上面的引用路徑,結(jié)合應(yīng)用相關(guān)源碼:
HomeTabActivity
源代碼(部分)侣诺;
/* HomeTabActivity.java */
public class HomeTabActivity extentds ... {
...
@Override
public void onBackPressed() {
...
if ((currentTime - touchTime) >= waitTime) {
Toast.makeText(this, "再按一次退出應(yīng)用", Toast.LENGTH_SHORT).show();
} else {
...
}
...
}
...
}
發(fā)現(xiàn)在連按兩次返回鍵退出應(yīng)用的功能代碼中殖演,將 HomeTabActivity
對象的引用傳入 Toast.makeText()
。
因此泄漏的原因是:
HomeTabActivity
對象被生命周期更長的 Toast$TN
對象所引用年鸳,導(dǎo)致其實(shí)際生命周期超出了所預(yù)期的生命周期剃氧。
其實(shí),站在 coder 的角度阻星,內(nèi)存泄漏本質(zhì)就是該死不死朋鞍,不論是什么具體形式導(dǎo)致了這種局面已添。
處方
本例中的泄漏是由于使用了不恰當(dāng)?shù)?Context
對象所致。
Android 中存在 Application Context 與 Activity Context 兩種具體的 Context
實(shí)例滥酥。前者的生命周期與應(yīng)用進(jìn)程的生命周期一樣更舞,比后者長。
因此坎吻,在使用 Toast 時缆蝉,應(yīng)該使用 Application Context ,就不會出現(xiàn)該死不死的對象瘦真,也就不存在內(nèi)存泄漏刊头。修復(fù)代碼如下:
public class HomeTabActivity extentds ... {
...
@Override
public void onBackPressed() {
...
if ((currentTime - touchTime) >= waitTime) {
Toast.makeText(getApplicationContext(), "再按一次退出應(yīng)用", Toast.LENGTH_SHORT).show();
} else {
...
}
...
}
...
}
附錄:
Android 常見內(nèi)存泄漏形式
- Activity 泄漏 - 內(nèi)部類
- Activity 泄漏 - 容器對象泄漏
- Activity 泄漏 - Static, Singleton
- 謹(jǐn)慎選擇合適的 Context
- 注意有生命周期對象的注銷
- 注意大胖子(Bitmap, WebView, Cursor)的及時回收