前言
內(nèi)存泄漏可以說是Android/ target=_blank class=infotextkey>安卓開發(fā)中常遇到的問題,追溯和排查其問題根源是進階的程序猿必須具備的一項技能。小盆友今天便與大家分享一下這方面的一些見解,如有理解錯誤或是不同見解,可以于評論區(qū)留言我們進行討論,如果喜歡給個贊鼓勵下吧。
1、JAVA內(nèi)存解析
要想知道內(nèi)存泄漏,需要先了解java中運行時內(nèi)存是怎么構(gòu)成的,才能知道是哪個地方導(dǎo)致。話不多說,先上圖

運行時的java內(nèi)存分為兩大塊:線程私有(藍色區(qū)域)、共享數(shù)據(jù)區(qū)(黃色區(qū)域)
線程私有:主要用于存儲各個線程私有的一些信息,包括:程序計數(shù)器、虛擬機棧、本地方法棧
共享數(shù)據(jù)區(qū):主要用于存儲公用的一些信息,包括:方法區(qū)(內(nèi)含常量池)、堆
- 程序計數(shù)器:讓程序中各個線程知道自己接下來需要執(zhí)行哪一行。在java中多線程為搶占式(因為cpu在某一時刻只會執(zhí)行一條線程),當線程切換時,需要繼續(xù)哪一行便由程序計數(shù)器告知。
- 舉個例子:A、B兩條線程,此時CPU執(zhí)行從A切換至B,過了段時間從B切換回A,此時A需要從上次暫停的地方繼續(xù)執(zhí)行,此時從哪一行執(zhí)行就是由程序計數(shù)器來提供。
- 值得一提:
- (1)若執(zhí)行java函數(shù)時,程序計數(shù)器記錄的是虛擬機字節(jié)碼的地址;
- (2)若執(zhí)行native方法時,程序計數(shù)器便置為了null。
- (3)在java虛擬機規(guī)范中,程序計數(shù)器是唯一沒有定義OutOfMemoryError。
- 虛擬機棧:描述的是java方法的內(nèi)存模型,平時說的“棧”其實就是虛擬機棧,其生命周期與線程相同。每個方法(不包含native方法)執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。
- 值得一提:在java虛擬機規(guī)范中,此處定義了兩個異常
- (1)StackOverFlowError (在遞歸中常看到,遞歸層級過深)
- (2)OutOfMemoryError
- 本地方法棧:是為虛擬機使用到的Native方法提供內(nèi)存空間。 有些虛擬機的實現(xiàn)直接把本地方法棧和虛擬機棧合二為一,比如主流的HotSpot虛擬機。
- 值得一提:在java虛擬機規(guī)范中,此處定義了兩個異常
- (1)StackOverFlowError (在遞歸中常看到,遞歸層級過深)
- (2)OutOfMemoryError
- 方法區(qū):主要存儲已加載是類信息(由ClassLoader加載)、常量、靜態(tài)變量、編譯后的代碼的一些信息。 GC在這里比較少出現(xiàn)在這塊區(qū)域。
- 堆:存放的是幾乎所有的對象實例和數(shù)組數(shù)據(jù)。 是虛擬機管理的最大的一塊內(nèi)存,是GC的主戰(zhàn)場,所以也叫“GC堆”、“垃圾堆” 。
- 值得一提:在java虛擬機規(guī)范中,此處定義了一個異常
- (1)OutOfMemoryError
- 運行時常量池:屬于“方法區(qū)”的一部分,用于存放編譯器生成的各種字面量和符號引用。
- 字面量:與Java語言層面的常量概念相近,包含文本字符串、聲明為final的常量值等。
- 符號引用:編譯語言層面的概念,包括以下3類:
- (1) 類和接口的全限定名
- (2)字段的名稱和描述符
- (3)方法的名稱和描述符
2、JAVA回收機制
java中是通過GC(Garbage Collection)來進行回收內(nèi)存,那jvm是如何確定一個對象能否被回收的呢?這里就需講到其回收使用的算法
(1) 引用計數(shù)算法
引用計數(shù)是垃圾收集器中的早期策略。在這種方法中,堆中每個對象實例都有一個引用計數(shù)。當一個對象被創(chuàng)建時,且將該對象實例分配給一個變量,該變量計數(shù)設(shè)置為1。當任何其它變量被賦值為這個對象的引用時,計數(shù)加1(a = b,則b引用的對象實例的計數(shù)器+1),當一個對象實例的某個引用超過了生命周期或者被設(shè)置為一個新值時,對象實例的引用計數(shù)器減1。任何引用計數(shù)器為0的對象實例可以被當作垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數(shù)器減1。
優(yōu)點:
引用計數(shù)收集器可以很快的執(zhí)行,交織在程序運行中。對程序需要不被長時間打斷的實時環(huán)境比較有利。
缺點:
無法檢測出循環(huán)引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數(shù)永遠不可能為0。例如下面代碼片段中,最后的Object實例已經(jīng)不在我們的代碼可控范圍內(nèi),但其引用仍為1,此時內(nèi)存便產(chǎn)生泄漏。
/**舉個例子**/ Object o1 = new Object() //Object的引用+1,此時計數(shù)器為1 Object o2; o2.o = o1; //Object的引用+1,此時計數(shù)器為2 o2 = null; o1 = null; //Object的引用-1,此時計數(shù)器為1
(2) 可達性分析算法

可達性分析算法是現(xiàn)在java的主流方法,通過一系列的GC ROOT為起始點,從一個GC ROOT開始,尋找對應(yīng)的引用節(jié)點,找到這個節(jié)點以后,繼續(xù)尋找這個節(jié)點的引用節(jié)點,當所有的引用節(jié)點尋找完畢之后,剩余的節(jié)點則被認為是沒有被引用到的節(jié)點,即無用的節(jié)點(即圖中的ObjD、ObjE、ObjF)。由此可知,即時引用成環(huán)也不會導(dǎo)致泄漏。
java中可作為GC Root的對象有:
1、方法區(qū)中靜態(tài)屬性引用的對象
2、方法區(qū)中常量引用的對象
3、本地方法棧JNI中引用的對象(Native對象)
4、虛擬機棧(本地變量表)中正在運行使用的引用
但是,可達性分析算法中不可達的對象,也并非一定要被回收。當GC第一次掃過這些對象的時候,他們處于“死緩”的階段。要真正執(zhí)行死刑,至少需要經(jīng)過兩次標記過程。 如果對象經(jīng)過可達性分析之后發(fā)現(xiàn)沒有與GC Roots相關(guān)聯(lián)的引用鏈,那他會被第一次標記,并經(jīng)歷一次篩選,這個對象的finalize方法會被執(zhí)行。如果對象沒有覆蓋finalize或者已經(jīng)被執(zhí)行過了。虛擬機也不會去執(zhí)行finalize方法。Finalize是對象逃獄的最后一次機會。
3、四種引用
說到底,內(nèi)存泄漏是因為引用的處理不正當導(dǎo)致的。所以,我們接下來需要老生常談一下java中四種引用,即:強軟弱虛(引用強度依次減弱)。
(1)強引用(Strong reference): 一般我們使用的都是強引用,例如:Object o = new Object();只要強引用還在,垃圾收集器就不會回收被引用的對象。
(2)軟引用(Soft Reference): 用來定義一些還有用但并非必須的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要內(nèi)存溢出之前,會將這些對象列入回收范圍進行第二次回收,如果回收后還是內(nèi)存不足,才會拋出內(nèi)存溢出。(即在內(nèi)存緊張時,會對其軟引用回收)
(3)弱引用(Weak Reference): 用來描述非必須對象。被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當垃圾收集器回收時,無論內(nèi)存是否足夠,都會回收掉被弱引用關(guān)聯(lián)的對象。(即GC掃過時,便將弱引用帶走)
(4)虛引用(Phantom Reference): 也稱為幽靈引用或者幻影引用,是最弱的引用關(guān)系。一個對象的虛引用根本不影響其生存時間,也不能通過虛引用獲得一個對象實例。虛引用的唯一作用就是這個對象被GC時可以收到一條系統(tǒng)通知。
軟引用與弱引用的抉擇
如果只是想避免OutOfMemory異常的發(fā)生,則可以使用軟引用。如果對于應(yīng)用的性能更在意,想盡快回收一些占用內(nèi)存比較大的對象,則可以使用弱引用。另外可以根據(jù)對象是否經(jīng)常使用來判斷選擇軟引用還是弱引用。如果該對象可能會經(jīng)常使用的,就盡量用軟引用。如果該對象不被使用的可能性更大些,就可以用弱引用。
4、小結(jié)
至此,我們知道內(nèi)存泄漏是因為堆內(nèi)存中的長生命周期的對象持有短生命周期對象的引用,盡管短生命周期對象已經(jīng)不再需要,但是因為長生命周期對象持有它的引用而導(dǎo)致不能被回收。
5、安卓內(nèi)存泄漏排查工具
所謂工欲善其事必先利其器,這一小節(jié)先簡述下所需借用到的內(nèi)存泄漏排查工具,如果已經(jīng)熟悉的話可以跳過。
(1) Android Profiler
這一工具是Android Studio自帶,可以查看cpu、內(nèi)存使用、網(wǎng)絡(luò)使用情況,Android Studio3.0中用于替代Android Monitor

① 強制執(zhí)行垃圾收集事件的按鈕。
② 捕獲堆轉(zhuǎn)儲的按鈕。
③ 記錄內(nèi)存分配的按鈕。
④ 放大時間線的按鈕。
⑤ 跳轉(zhuǎn)到實時內(nèi)存數(shù)據(jù)的按鈕。
⑥ 事件時間線顯示活動狀態(tài)、用戶輸入事件和屏幕旋轉(zhuǎn)事件。
⑦ 內(nèi)存使用時間表,其中包括以下內(nèi)容:
• 每個內(nèi)存類別使用多少內(nèi)存的堆棧圖,如左邊的y軸和頂部的顏色鍵所示。
• 虛線表示已分配對象的數(shù)量,如右側(cè)y軸所示。
• 每個垃圾收集事件的圖標。
(2) MAT(Memory Analyzer Tool)
MAT用于鎖定哪里泄漏。因為從Android Profiler中,知道了泄漏,但比較難鎖定具體哪個地方導(dǎo)致了泄漏,所以借助MAT來鎖定,具體使用待會會借助一個例子配合Android Profiler來介紹,稍安勿躁。
下載地址:www.eclipse.org/mat/downloa…
6、內(nèi)存泄漏檢查與解決流程
經(jīng)過前面的一段理論,可能很多小伙伴都有些不耐煩了,現(xiàn)在便來真正的操作。
溫馨提示:理論是進階中必要的支持,否則只是知其然而不知其所以然。
(1)第一步:對待檢測功能掃雷式操作
當我們需要檢查一塊模塊,或是整個App哪個地方有內(nèi)存泄漏時,有時會比較茫然,有些大海撈針的感覺,畢竟泄漏不是每個頁面都會有,而且有時是一個功能才會導(dǎo)致泄漏,所以我們可以采取“掃雷式操作”,也就是在需要檢查的頁面和功能中隨便先使用一番,舉個例子:假設(shè)檢查MainActivity泄漏情況,可以登錄進入后,此時來到了MainActivity,后又登出,再次登錄進入MainActivity。
(2)第二步:借助 Android Profiler獲得內(nèi)存快照
使用Android Profiler的GC功能,強制進行垃圾回收,再dump下內(nèi)存("Android Profiler功能簡介"圖的②按鈕)。然后等待一段時間,會出現(xiàn)圖中紅色框部分:

在這里得到的頁面,其實比較難直觀獲得內(nèi)存分析的數(shù)據(jù),最多只是選擇“Arrange by package”按照包進行排序,然后進到自己的包下,查看應(yīng)用內(nèi)的activity的引用數(shù)是否正常,來判斷其是否有正常回收

圖中列的說明
Alloc Cout : 對象數(shù)
Shallow Size : 對象占用內(nèi)存大小
Retained Set : 對象引用組占用內(nèi)存大小(包含了這個對象引用的其他對象)
(3)第三步:借助Android Studio分析
至此,我們還是沒得到直觀的內(nèi)存分析數(shù)據(jù),我們需要借助更專業(yè)的工具。我們現(xiàn)將通過下圖中紅框內(nèi)的按鈕,將剛才的內(nèi)存快照保存為hprof文件。

將保存好的hprof文件拖進AS中,勾選“Detect Leaked Activities”,然后點擊綠色按鈕進行分析。

如果有內(nèi)存泄漏的話,會出現(xiàn)如下圖的情況。圖中很清晰的可以看到,這里出現(xiàn)了MainActivity的泄漏。并且觀察到這個MainActivity可能不止一個對象存在,可能是我們上次退出程序的時候發(fā)生了泄漏,導(dǎo)致它不能回收。而在此打開app,系統(tǒng)會創(chuàng)建新的MainActivity。但至此我們只是知道MainActivity泄漏了,不知具體是哪里導(dǎo)致了MainActivity泄漏,所以需要借助MAT來進一步分析。

(4)第四步:hprof文件轉(zhuǎn)換
在使用MAT打開hprof文件前先要對剛才保存的hprof文件進行轉(zhuǎn)換。通過終端,借助轉(zhuǎn)換工具hprof-conv(在sdk/platform-tools/hprof-conv),使用命令行:
hprof-conv -z src dst
-z:排除不是app的內(nèi)存,比如Zygote
src:需要進行轉(zhuǎn)換的hprof的文件路徑
dst:轉(zhuǎn)換后的文件路徑(文件后綴還是.hprof)
(5)第五步:通過MAT進行具體分析 在MAT中打開轉(zhuǎn)換了的hprof文件,如下圖

打開后會看到如下圖!
我們需要進入到"Histogram"來分析,點擊下圖中的按鈕

打開"Histogram"后,會看到下圖,在紅框中輸入在AS中觀察到的泄漏的類,例如上面得知的MainActivity

然后將搜索得到的結(jié)果進行合并,排除“軟”、“弱”、“虛”引用對象,右鍵點擊搜索到的結(jié)果,選擇如下圖的選項

得到合并結(jié)果如下

從分析結(jié)果可知,MainActivity是因為com.netease.nimlib.g.e中的一個hashMap持有導(dǎo)致,這里的e類是第三方庫的類,顯然已被混淆,造成泄漏無非兩種可能,一種是第三方庫的bug,一種是自己使用不當,例如忘記解綁操作等。具體的打斷這個持有需要按照自己的代碼進行分析,實例中的問題是因為使用第三方庫注冊后,在退出頁面沒有進行注銷導(dǎo)致的。
當我們解決完后,可以再次進行一輪內(nèi)存快照,直到?jīng)]有內(nèi)存泄漏,過程會比較枯燥,但一點點的解決泄漏最終會給app一個質(zhì)的飛躍。
7、常見的內(nèi)存泄漏原因
(1)集合類
集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機制,導(dǎo)致內(nèi)存被占用。如果這個集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。
(2)單例模式
不正確使用單例模式是引起內(nèi)存泄露的一個常見問題,單例對象在被初始化后將在 JVM 的整個生命周期中存在(以靜態(tài)變量的方式),如果單例對象持有外部對象的引用,那么這個外部對象將不能被 JVM 正常回收,導(dǎo)致內(nèi)存泄露。
public class SingleTest{ private static SingleTest instance; private Context context; private SingleTest(Context context){ this.context = context; } public static SingleTest getInstance(Context context){ if(instance != null){ instance = new SingleTest(context); } return instance; } }
這里如果傳遞Activity作為Context來獲得單例對象,那么單例持有Activity的引用,導(dǎo)致Activity不能被釋放。 不要直接對 Activity 進行直接引用作為成員變量,如果允許可以使用Application。 如果不得不需要Activity作為Context,可以使用弱引用WeakReference,相同的,對于Service 等其他有自己生命周期的對象來說,直接引用都需要謹慎考慮是否會存在內(nèi)存泄露的可能。
(3)未關(guān)閉或釋放資源
BroadcastReceiver,ContentObserver,F(xiàn)ileObserver,Cursor,Callback等在 Activity onDestroy 或者某類生命周期結(jié)束之后一定要 unregister 或者 close 掉,否則這個 Activity 類會被 system 強引用,不會被內(nèi)存回收。值得注意的是,關(guān)閉的語句必須在finally中進行關(guān)閉,否則有可能因為異常未關(guān)閉資源,致使activity泄漏
(4)Handler
只要 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對象將被線程 MessageQueue 一直持有。特別是handler執(zhí)行延遲任務(wù)。所以,Handler 的使用要尤為小心,否則將很容易導(dǎo)致內(nèi)存泄露的發(fā)生。
public class MainActivity extends AppCompatActivity { private Handler mHandler = new Handler(){ @Override public void handleMessage(Message msg) { //do something } }; private void loadData(){ //do request Message message = Message.obtain(); mHandler.sendMessage(message); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loadData(); } }
這種創(chuàng)建Handler的方式會造成內(nèi)存泄漏,由于mHandler是Handler的非靜態(tài)匿名內(nèi)部類的實例,所以它持有外部類Activity的引用,我們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那么當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,所以導(dǎo)致該Activity的內(nèi)存資源無法及時回收,引發(fā)內(nèi)存泄漏,所以另外一種做法為:
public class MainActivity extends AppCompatActivity { private MyHandler mHandler = new MyHandler(this); private void loadData() { //do request Message message = Message.obtain(); mHandler.sendMessage(message); } private static class MyHandler extends Handler { private WeakReference<Context> reference; public MyHandler(Context context) { reference = new WeakReference<Context>(context); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); MainActivity mainActivity = (MainActivity) reference.get(); if (mainActivity != null) { //do something to update UI via mainActivity } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); loadData(); } }
創(chuàng)建一個靜態(tài)Handler內(nèi)部類,然后對Handler持有的對象使用弱引用,這樣在回收時也可以回收Handler持有的對象,這樣雖然避免了Activity泄漏,不過Looper線程的消息隊列中還是可能會有待處理的消息,所以我們在Activity的Destroy時或者Stop時應(yīng)該移除消息隊列中的消息,
@Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null); }
使用mHandler.removeCallbacksAndMessages(null);是移除消息隊列中所有消息和所有的Runnable。當然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。
(5)Thread
和handler一樣,線程也是造成內(nèi)存泄露的一個重要的源頭。線程產(chǎn)生內(nèi)存泄露的主要原因在于線程生命周期的不可控。比如線程是 Activity 的內(nèi)部類,則線程對象中保存了 Activity 的一個引用,當線程的 run 函數(shù)耗時較長沒有結(jié)束時,線程對象是不會被銷毀的,因此它所引用的老的 Activity 也不會被銷毀,因此就出現(xiàn)了內(nèi)存泄露的問題。
(6)系統(tǒng)bug
比如InputMethodManager,會持有activity而沒釋放,導(dǎo)致泄漏,需要通過反射進行打斷。
最后
好啦,文章寫到這里就結(jié)束了,如果你覺得文章寫得不錯就給個贊唄?如果你覺得那里值得改進的,請給我留言。一定會認真查詢,修正不足。謝謝。