Android 開發(fā)進(jìn)階之『清除應(yīng)用中的內(nèi)存泄漏』

Android 內(nèi)存管理機(jī)制

Android 的內(nèi)存管理機(jī)制可以簡單概括為:系統(tǒng)沒有為內(nèi)存提供交換區(qū)鹦筹,它使用 pagingmemory-mapping(mmapping) 來管理內(nèi)存宋列。

對開發(fā)來說砰嘁,上面的管理機(jī)制意味著:

  1. 徹底釋放內(nèi)存資源的唯一方法是釋放對象的引用替久,使對象可以被 GC(garbage collector) 回收。
  2. 有一種例外情況:沒有任何修改的文件,比如代碼本身,映射進(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)存不再使用卻沒有得到釋放。

Memory Leaks

內(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類型的錯誤:

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ù)這個操作路徑赶促!

這里需要提醒的是:

  1. 不是任意一臺設(shè)備都可以復(fù)現(xiàn)所有泄漏液肌,使用同款設(shè)備嘗試。
  2. 要記錄測試數(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 文件)婿滓。
  3. 如果每次泄漏的內(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 :

object graph to 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 TreeHistogram 這兩個功能睦番。

MAT 相關(guān)功能簡介

使用 MAT 打開前面拿到的 hprof 文件:

MAT_Overview.png

首先看到的是 Overview 頁面。
里面 Details 部分顯示了堆的一些基本信息耍属,以及 Unreachable Objects Histogram 入口托嚣,其中列出了堆中所有 Unreferenced 對象。

Actions 部分厚骗,有HistogramDominator Tree 的入口示启,前者更關(guān)注堆中對象的個數(shù),后者更關(guān)注堆中對象的類型领舰。其中列出的對象都是 Referenced 對象夫嗓。被泄漏的對象一定是從里面找。

尋找被泄漏對象冲秽,可以從兩個方向下手:

方式一舍咖、從對象個數(shù)入手

如果前面的重復(fù)次數(shù) n 已知的話,可以先從對象個數(shù)入手锉桑。重復(fù)一次泄漏路徑谎仲,就會泄漏一次對象,所以重復(fù) n 次刨仑,泄漏的對象個數(shù)應(yīng)該為 n 個郑诺。

打開 Histogram :


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 附近的類梁呈,

Objects leaked shown in histogram

不難發(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

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 一致扮饶。

MAT_dt_leaked.png

修復(fù)泄漏(治病)

找到被泄漏的對象后乍构,接著要算出從該對象到 GC roots 的最短強(qiáng)引用路徑甜无,找到本不該存在的路徑,對照相應(yīng)源碼蜡吧,修復(fù)掉錯誤的代碼邏輯,也就剔除了這個內(nèi)存泄漏占键。

找病根

在 MAT 中如何看到一個對象到 GC root 的最短強(qiáng)引用路徑呢昔善?

  • Histogram 中查看
Show shortest paths to GC roots exclude weak references

在被泄漏類上面,點(diǎn)擊右鍵菜單中的 Merge Shartest Paths to GC Roots --> exclude weak references 畔乙。就會看到這個 java 類中所有無法被釋放的對象的 GC roots 君仆,點(diǎn)開每條路徑,可以看到引用關(guān)系牲距。

Paths to GC roots with detail infomation

從上圖返咱,可以看到被泄漏的 HomeTabActivity 對象都是同一個 GC root 。

  • Dominator Tree 中查看

在被泄漏對象上面牍鞠,通過右鍵菜單咖摹,選擇 Path to GC Roots --> exclude weak references 可以看到該對象到 GC root 的一條路徑。

MAT_dominator_tree_root.png

與通過 Dominator Tree 找到的路徑一致难述。

Paths to GC roots with detail infomation

前面說這條路徑是不應(yīng)該存在的萤晴,但是是什么原因?qū)е缕涑霈F(xiàn)呢?接下來我們分析泄漏原因胁后。

MAT_dt_gc_root.png

我們看到位于地址 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 ContextActivity 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)部類
內(nèi)部類
  • Activity 泄漏 - 容器對象泄漏
容器對象泄漏
  • Activity 泄漏 - Static, Singleton
Static, Singleton
  • 謹(jǐn)慎選擇合適的 Context
合適的 Context
  • 注意有生命周期對象的注銷
有生命周期對象的注銷
  • 注意大胖子(Bitmap, WebView, Cursor)的及時回收
Bitmap
WebView
Cursor

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诸尽,隨后出現(xiàn)的幾起案子原杂,更是在濱河造成了極大的恐慌,老刑警劉巖您机,帶你破解...
    沈念sama閱讀 206,013評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件穿肄,死亡現(xiàn)場離奇詭異,居然都是意外死亡际看,警方通過查閱死者的電腦和手機(jī)咸产,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仲闽,“玉大人脑溢,你說我怎么就攤上這事±敌溃” “怎么了焚志?”我有些...
    開封第一講書人閱讀 152,370評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長畏鼓。 經(jīng)常有香客問我酱酬,道長,這世上最難降的妖魔是什么云矫? 我笑而不...
    開封第一講書人閱讀 55,168評論 1 278
  • 正文 為了忘掉前任膳沽,我火速辦了婚禮,結(jié)果婚禮上让禀,老公的妹妹穿的比我還像新娘挑社。我一直安慰自己,他們只是感情好巡揍,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評論 5 371
  • 文/花漫 我一把揭開白布痛阻。 她就那樣靜靜地躺著,像睡著了一般腮敌。 火紅的嫁衣襯著肌膚如雪阱当。 梳的紋絲不亂的頭發(fā)上俏扩,一...
    開封第一講書人閱讀 48,954評論 1 283
  • 那天,我揣著相機(jī)與錄音弊添,去河邊找鬼录淡。 笑死,一個胖子當(dāng)著我的面吹牛油坝,可吹牛的內(nèi)容都是我干的嫉戚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,271評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼澈圈,長吁一口氣:“原來是場噩夢啊……” “哼彬檀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起瞬女,我...
    開封第一講書人閱讀 36,916評論 0 259
  • 序言:老撾萬榮一對情侶失蹤窍帝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拆魏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盯桦,經(jīng)...
    沈念sama閱讀 43,382評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡慈俯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評論 2 323
  • 正文 我和宋清朗相戀三年渤刃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贴膘。...
    茶點(diǎn)故事閱讀 37,989評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡卖子,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刑峡,到底是詐尸還是另有隱情洋闽,我是刑警寧澤,帶...
    沈念sama閱讀 33,624評論 4 322
  • 正文 年R本政府宣布突梦,位于F島的核電站诫舅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏宫患。R本人自食惡果不足惜刊懈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娃闲。 院中可真熱鬧虚汛,春花似錦、人聲如沸皇帮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽属拾。三九已至将谊,卻和暖如春冷溶,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瓢娜。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評論 1 260
  • 我被黑心中介騙來泰國打工挂洛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人眠砾。 一個月前我還...
    沈念sama閱讀 45,401評論 2 352
  • 正文 我出身青樓虏劲,卻偏偏與公主長得像,于是被迫代替她去往敵國和親褒颈。 傳聞我的和親對象是個殘疾皇子柒巫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評論 2 345

推薦閱讀更多精彩內(nèi)容