內存泄漏弄個明白

若是你參加面試,面試官常常會問到你的一個問題多是:你在開發過程當中,有過排除內存泄漏的經驗嗎?對於一個合格的Android/C/Java開發老手,這個問題想必已經深刻你的心;如果一名新手或者一直對內存泄漏這個東西模模糊糊的工程師,你的答案可能讓面試官並不滿意,這裏將從底到上對內存泄漏的緣由、排查方法和一些經驗爲你作一次完整的解剖。html

處理內存泄漏的問題是將軟件作到極致的一個必須的步驟,尤爲是那種將被用戶高強度使用的軟件。java

 

一個簡單的C和Android的例子jquery


一個最簡單的C的內存泄漏的例子:android

char *ptr1 = (char *)malloc(10); char *ptr2 = (char *)malloc(10); ptr2 = ptr1; free(ptr1)

這裏最後發生了10個字節的內存泄漏,那麼到底發生了什麼?程序員

首先各自分配了兩塊10個字節的內存,分別用叫ptr1和ptr2的指針指向這兩塊內存(就像是java中的引用),而後呢讓ptr2也指向一開始ptr1指向的那塊內存(這時候ptr1和ptr2都指向了ptr1一開始指向的那個10個字節的內存),最後用free將ptr1指向的那塊內存給釋放了——>結果就是一開始ptr2指向的那塊內存發生了泄漏(沒人用了卻又回收不掉)面試

 

可能你會說Java有內存垃圾回收機制,不要考慮誰分配和釋放的訪問,那麼下面這個例子就會讓你明白你錯了:網絡

複製代碼
public class PendingOrderManager { private static PendingOrderManager instance; private Context mContext; public PendingOrderManager(Context context) { this.mContext = context; } public static PendingOrderManager getInstance(Context context) { if (instance == null) { instance = new PendingOrderManager(context); } return instance; }

   public void func(){
...
}
... }
複製代碼

而後讓你的某個Activity去使用這個PendingOrderManager單例,而且某個時候退出這個Activity:app

//belong to some Activity
PendingOrderManager.getInstance(this).func(); ... finish()

 

這個時候內存泄漏已經發生:你退出了你的這個Activity本覺得java的垃圾回收會將它釋放,但實際上Activity一直被PendingOrderManager持有着。Acitivity這個Context被長生命週期個體(單例一旦被建立就是整個app的生命週期)持有致使了這個Context發生了內存泄漏。異步

這個例子和上面的例子是相通的,上面的C的例子由於忘記了手動執行free一個10字節內存致使內存泄漏。而下面這個例子是垃圾回收機制「故意忘記」了回收Context的內存而致使了內存泄漏。下面兩節將對這個裏面到底發生了什麼進行說明。函數

 

靜態、堆和棧


編譯原理說軟件內存分配的時候通常會放在三種位置:靜態存儲區域、堆和棧,他們的位置、功能、速度都各不相同,區別以下:

 

  • 靜態存儲區:內存在程序編譯的時候就已經分配好,這塊內存在程序整個運行期間都存在。它主要存放靜態數據、全局static數據和常量
  • 棧:就是CPU的寄存器(並非內存),特色是容量很小可是速度最快,函數或者方法的的方法體內聲明的變量或者指向對象的引用、局部變量即分配在這裏,生命週期到該函數或者方法體尾部即止
  • 堆:就是動態內存分配去(就是實體的內存RAM),C中malloc和fee,java中的new和垃圾回收直接操做的就是這裏的區域,類的成員變量分配在這裏

從上面便可看出靜態存儲區域是編譯時已經分配好的,棧是CPU自動控制的,那麼咱們所討論的內存泄漏的問題實際上就是分配在堆裏面的內存出現了問題,通常問題在於兩點:

  1. 快速不斷的進行new操做。好比Android的自定義View的時候你在onDraw裏面new出對象,就會致使這個自定義View的繪製特別卡,這是由於onDraw是快速重複執行的方法,在這個方法裏面每次都new出對象會致使接二連三的new出新的對象,也致使gc也在不斷的執行從而不斷的回收堆內存。因爲堆位於內存RAM上,這樣子就致使了內存的不斷的分配和回收消耗了CPU,同時致使了內存出現「空洞」(由於堆內存不是連續的)
  2. 忘記釋放。若是你忘記了手動釋放應該釋放的內存,或者gc誤判致使沒有釋放本應該釋放的內存,那麼久致使了內存泄漏。因爲Android給一個app可在堆上(能夠在AndroidManifest設置一個largeHeap="true"增大可分配量)分配的內存量是有限的,若是內存泄漏不斷的發生,總有一天會消耗完畢,從而致使OOM

 

Java有了垃圾回收(GC)爲何任而後內存泄漏


在Java中,內存的分配是由程序完成的,而內存的釋放是由垃圾收集器(Garbage Collection,GC)完成的,程序員不須要經過調用函數來釋放內存,但它只能回收無用而且再也不被其它對象引用的那些對象所佔用的空間。可是誤判是常常發生的,有些內存實際上已經沒有用處了,可是GC並不知道。這裏簡單介紹下GC的機制:

上面一節說過棧上的局部變量能夠引用堆上的分配的內存,因此GC發生的時候,通常是遍歷一下靜態存儲區、棧從而列出全部堆上被他們引用的內存(對象)集合,這些內存都是有個引用計數,那麼除此以外,其餘的內存就是沒有被引用的(或者說引用計數歸零),這些內存就是要被釋放的,隨後GC開始清理這些內存(對象)

 

那麼這裏第一節的兩個例子就很好理解了,那個單例模式因爲生命週期太長(能夠把他看做一個虛擬的棧中的局部變量)而且一直引用了Context(即Activity),因此GC的時候發現這個Activity的引用計數仍是大於1,因此回收內存的時候把他跳過,但實際上咱們已經不須要這塊內存了。這樣就致使了內存泄漏。

 

Android使用弱引用和完美退出app的方法


從上面來看,內存泄漏由於對象被別人引用了而致使,java爲了不這種問題(假如你的單例模式必需要傳入個Context),特意提供了幾個特殊引用類型,其中一個叫作弱引用WeakReference,當它引用一個對象的時候,即便該WeakReference的生命週期更長,可是隻要發生GC,它就當即釋放所被引用的內存而不會繼續持有。

這裏有一個經常使用的例子:

一般咱們會在自定義的Application中來記住app中建立的Activity,從而中途在某個Activity中須要徹底退出app時能夠徹底的銷燬全部已經打開的Activity,這裏咱們能夠對自定義Application改造,讓其只有一個對Activity的弱引用的HashMap,大體的代碼以下:

複製代碼
public class CustomApplication extends Application { private HashMap> activityList = new HashMap>(); private static CustomApplication instance; public static CustomApplication getInstance() { return instance; } public void addActivity(Activity activity) { if (null != activity) { L.d("********* add Activity " + activity.getClass().getName()); activityList.put(activity.getClass().getName(), new WeakReference<>(activity)); } } public void removeActivity(Activity activity) { if (null != activity) { L.d("********* remove Activity " + activity.getClass().getName()); activityList.remove(activity.getClass().getName()); } } public void exit() { for (String key : activityList.keySet()) { WeakReference activity = activityList.get(key); if (activity != null && activity.get() != null) { L.d("********* Exit " + activity.get().getClass().getSimpleName()); activity.get().finish(); } } System.exit(0); android.os.Process.killProcess(android.os.Process.myPid()); } }
複製代碼

 

咱們在自定義的Activity的基類BaseActivity中的onCreate執行:

CustomApplication.getInstance().addActivity(this);

在BaseActivity的onDestroy中執行:

CustomApplication.getInstance().removeActivity(this);

這樣子自定義Application就能不泄露的持有全部打開的Activity的引用,同時當你須要中途退出app的時候,只須要執行:

//完美退出程序方法
CustomApplication.getInstance().exit();

 

哪些狀況會致使內存泄漏


到此你應該對內存泄漏的本質已經有所瞭解了,這裏列舉出一些會致使內存泄漏的地方,能夠做爲排查內存泄漏的一個checklist

  • 某個集合類(List)被一個static變量引用,同時這個集合類沒有刪除本身內部的元素
  • 單例模式持有外部本應該被釋放的對象(第一節中那個例子)
  • Android特殊組件或者類忘記釋放,好比:BraodcastReceiver忘記解註冊、Cursor忘記銷燬、Socket忘記close、TypedArray忘記recycle、callback忘記remove。若是你本身定義了一個類,最好不要直接將一個Activity類型做爲他的屬性,若是必需要用,要麼處理好釋放的問題,要麼使用弱引用
  • Handler。只要 Handler 發送的 Message 還沒有被處理,則該 Message 及發送它的 Handler 對象將被線程 MessageQueue 一直持有。因爲 Handler 屬於 TLS(Thread Local Storage) 變量, 生命週期和 Activity 是不一致的。所以這種實現方式通常很難保證跟 View 或者 Activity 的生命週期保持一致,故很容易致使沒法正確釋放。如上所述,Handler 的使用要尤其當心,不然將很容易致使內存泄露的發生。
  • Thread。若是Thread的run方法一直在循環的執行不停,而該Thread又持有了外部變量,那麼這個外部變量即發生內存泄漏。
  • 網絡請求或者其餘異步線程。以前Volley會有這樣的一個問題,在Volley的response來到以前若是Activity已經退出了並且response裏面含有Activity的成員變量,會致使該Activity發生內存泄漏,該問題一直沒有找到合適的解決辦法。不過看來Volley官網已經注意到這個問題了,目前最新的版本已經fix this leak

 

使用leakcanary


 

以前Android開發一般使用MAT內存分析工具來排查heap的問題,之類的文章比較多,你們能夠本身找。這裏推薦一個叫作leakcanary的工具,他能夠集成在你的代碼裏面。這個東西你們能夠參考:

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0509/2854.html

 

http://www.cnblogs.com/soaringEveryday/p/5035366.html

相關文章
相關標籤/搜索