對于開發(fā)老手扇调,這個問題想必已經(jīng)深入你的心;若是一名新手或者一直對內(nèi)存泄漏這個東西模模糊糊的工程師在讶,你的答案可能讓面試官并不滿意煞抬,這里將從底到上對內(nèi)存泄漏的原因、排查方法和一些經(jīng)驗為你做一次完整的解剖构哺。
處理內(nèi)存泄漏的問題是將軟件做到極致的一個必須的步驟此疹,尤其是那種將被用戶高強度使用的軟件。
案例:
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:
//belong to some Activity
PendingOrderManager.getInstance(this).func();
...
finish()
這個時候內(nèi)存泄漏已經(jīng)發(fā)生:你退出了你的這個Activity本以為java的垃圾回收會將它釋放,但實際上Activity一直被PendingOrderManager持有著湖笨。Acitivity這個Context被長生命周期個體(單例一旦被創(chuàng)建就是整個app的生命周期)持有導(dǎo)致了這個Context發(fā)生了內(nèi)存泄漏旗扑。
這個例子和上面的例子是相通的,上面的C的例子因為忘記了手動執(zhí)行free一個10字節(jié)內(nèi)存導(dǎo)致內(nèi)存泄漏慈省。而下面這個例子是垃圾回收機制“故意忘記”了回收Context的內(nèi)存而導(dǎo)致了內(nèi)存泄漏臀防。下面兩節(jié)將對這個里面到底發(fā)生了什么進行說明。
靜態(tài)边败、堆和棧
編譯原理說軟件內(nèi)存分配的時候一般會放在三種位置:靜態(tài)存儲區(qū)域袱衷、堆和棧,他們的位置笑窜、功能致燥、速度都各不相同,區(qū)別如下:
- 靜態(tài)存儲區(qū):內(nèi)存在程序編譯的時候就已經(jīng)分配好排截,這塊內(nèi)存在程序整個運行期間都存在嫌蚤。它主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和常量
- 棧:就是CPU的寄存器(并不是內(nèi)存)断傲,特點是容量很小但是速度最快脱吱,函數(shù)或者方法的的方法體內(nèi)聲明的變量或者指向?qū)ο蟮囊谩⒕植孔兞考捶峙湓谶@里认罩,生命周期到該函數(shù)或者方法體尾部即止
- 堆:就是動態(tài)內(nèi)存分配去(就是實體的內(nèi)存RAM)箱蝠,C中malloc和fee,java中的new和垃圾回收直接操作的就是這里的區(qū)域垦垂,類的成員變量分配在這里
從上面即可看出靜態(tài)存儲區(qū)域是編譯時已經(jīng)分配好的宦搬,棧是CPU自動控制的,那么我們所討論的內(nèi)存泄漏的問題實際上就是分配在堆里面的內(nèi)存出現(xiàn)了問題乔外,一般問題在于兩點:
- 快速不斷的進行new操作床三。比如Android的自定義View的時候你在onDraw里面new出對象,就會導(dǎo)致這個自定義View的繪制特別卡杨幼,這是因為onDraw是快速重復(fù)執(zhí)行的方法撇簿,在這個方法里面每次都new出對象會導(dǎo)致連續(xù)不斷的new出新的對象聂渊,也導(dǎo)致gc也在不斷的執(zhí)行從而不斷的回收堆內(nèi)存。由于堆位于內(nèi)存RAM上四瘫,這樣子就導(dǎo)致了內(nèi)存的不斷的分配和回收消耗了CPU汉嗽,同時導(dǎo)致了內(nèi)存出現(xiàn)“空洞”(因為堆內(nèi)存不是連續(xù)的)
- 忘記釋放。如果你忘記了手動釋放應(yīng)該釋放的內(nèi)存找蜜,或者gc誤判導(dǎo)致沒有釋放本應(yīng)該釋放的內(nèi)存饼暑,那么久導(dǎo)致了內(nèi)存泄漏。由于Android給一個app可在堆上(可以在AndroidManifest設(shè)置一個largeHeap="true"增大可分配量)分配的內(nèi)存量是有限的洗做,如果內(nèi)存泄漏不斷的發(fā)生弓叛,總有一天會消耗完畢,從而導(dǎo)致OOM
Java有了垃圾回收(GC)為什么依然會內(nèi)存泄漏
在Java中诚纸,內(nèi)存的分配是由程序完成的撰筷,而內(nèi)存的釋放是由垃圾收集器(Garbage Collection,GC)完成的畦徘,程序員不需要通過調(diào)用函數(shù)來釋放內(nèi)存毕籽,但它只能回收無用并且不再被其它對象引用的那些對象所占用的空間。但是誤判是經(jīng)常發(fā)生的井辆,有些內(nèi)存實際上已經(jīng)沒有用處了关筒,但是GC并不知道。這里簡單介紹下GC的機制:
上面一節(jié)說過棧上的局部變量可以引用堆上的分配的內(nèi)存杯缺,所以GC發(fā)生的時候蒸播,一般是遍歷一下靜態(tài)存儲區(qū)、棧從而列出所有堆上被他們引用的內(nèi)存(對象)集合萍肆,這些內(nèi)存都是有個引用計數(shù)廉赔,那么除此之外,其他的內(nèi)存就是沒有被引用的(或者說引用計數(shù)歸零)匾鸥,這些內(nèi)存就是要被釋放的蜡塌,隨后GC開始清理這些內(nèi)存(對象)
那么這里第一節(jié)的兩個例子就很好理解了,那個單例模式由于生命周期太長(可以把他看作一個虛擬的棧中的局部變量)并且一直引用了Context(即Activity)勿负,所以GC的時候發(fā)現(xiàn)這個Activity的引用計數(shù)還是大于1馏艾,所以回收內(nèi)存的時候把他跳過,但實際上我們已經(jīng)不需要這塊內(nèi)存了奴愉。這樣就導(dǎo)致了內(nèi)存泄漏琅摩。
Android使用弱引用和完美退出app的方法
從上面來看,內(nèi)存泄漏因為對象被別人引用了而導(dǎo)致锭硼,java為了避免這種問題(假如你的單例模式必須要傳入個Context)房资,特地提供了幾個特殊引用類型,其中一個叫做弱引用WeakReference檀头,當它引用一個對象的時候轰异,即使該WeakReference的生命周期更長岖沛,但是只要發(fā)生GC,它就立即釋放所被引用的內(nèi)存而不會繼續(xù)持有搭独。
這里有一個常用的例子:
通常我們會在自定義的Application中來記住app中創(chuàng)建的Activity婴削,從而中途在某個Activity中需要完全退出app時可以完全的銷毀所有已經(jīng)打開的Activity,這里我們可以對自定義Application改造牙肝,讓其只有一個對Activity的弱引用的HashMap唉俗,大致的代碼如下:
public class CustomApplication extends Application {
private HashMap<String, WeakReference<Activity>> activityList = new HashMap<String, WeakReference<Activity>>();
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> 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執(zhí)行:
CustomApplication.getInstance().addActivity(this);
在BaseActivity的onDestroy中執(zhí)行:
CustomApplication.getInstance().removeActivity(this);
哪些情況會導(dǎo)致內(nèi)存泄漏
到此你應(yīng)該對內(nèi)存泄漏的本質(zhì)已經(jīng)有所了解了,這里列舉出一些會導(dǎo)致內(nèi)存泄漏的地方配椭,可以作為排查內(nèi)存泄漏的一個checklist
- 某個集合類(List)被一個static變量引用虫溜,同時這個集合類沒有刪除自己內(nèi)部的元素
- 單例模式持有外部本應(yīng)該被釋放的對象(第一節(jié)中那個例子)
- Android特殊組件或者類忘記釋放,比如:BraodcastReceiver忘記解注冊股缸、Cursor忘記銷毀吼渡、Socket忘記close、TypedArray忘記recycle乓序、callback忘記remove。如果你自己定義了一個類坎背,最好不要直接將一個Activity類型作為他的屬性替劈,如果必須要用,要么處理好釋放的問題得滤,要么使用弱引用
- Handler陨献。只要 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對象將被線程 MessageQueue 一直持有懂更。由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的眨业。因此這種實現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無法正確釋放沮协。如上所述龄捡,Handler 的使用要尤為小心,否則將很容易導(dǎo)致內(nèi)存泄露的發(fā)生慷暂。
- Thread聘殖。如果Thread的run方法一直在循環(huán)的執(zhí)行不停,而該Thread又持有了外部變量行瑞,那么這個外部變量即發(fā)生內(nèi)存泄漏奸腺。
-
網(wǎng)絡(luò)請求或者其他異步線程。之前Volley會有這樣的一個問題血久,在Volley的response來到之前如果Activity已經(jīng)退出了而且response里面含有Activity的成員變量突照,會導(dǎo)致該Activity發(fā)生內(nèi)存泄漏,該問題一直沒有找到合適的解決辦法氧吐。不過看來Volley官網(wǎng)已經(jīng)注意到這個問題了讹蘑,目前最新的版本已經(jīng)fix this leak
使用leakcanary
之前Android開發(fā)通常使用MAT內(nèi)存分析工具來排查heap的問題末盔,之類的文章比較多,大家可以自己找衔肢。這里推薦一個叫做leakcanary的工具庄岖,他可以集成在你的代碼里面。這個東西大家可以參考:
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0509/2854.html
特別鳴謝
文章改編于: 內(nèi)存泄漏弄個明白 - soaringEveryday - 博客園
http://www.cnblogs.com/soaringEveryday/p/5035366.html