一、什么是內(nèi)存泄漏
內(nèi)存泄露 memory leak,是指程序在申請(qǐng)內(nèi)存后硝拧,無(wú)法釋放已申請(qǐng)的內(nèi)存空間,一次內(nèi)存泄露危害可以忽略葛假,但內(nèi)存泄露堆積后果很?chē)?yán)重障陶,無(wú)論多少內(nèi)存,遲早會(huì)被占光。memory leak會(huì)最終會(huì)導(dǎo)致out of memory聊训!
內(nèi)存泄漏是指你向系統(tǒng)申請(qǐng)分配內(nèi)存進(jìn)行使用(new)咸这,可是使用完了以后卻不歸還(delete),結(jié)果你申請(qǐng)到的那塊內(nèi)存你自己也不能再訪(fǎng)問(wèn)(也許你把它的地址給弄丟了)魔眨,而系統(tǒng)也不能再次將它分配給需要的程序。一個(gè)盤(pán)子用盡各種方法只能裝4個(gè)果子酿雪,你裝了5個(gè)遏暴,結(jié)果掉倒地上不能吃了。這就是溢出指黎!比方說(shuō)棧朋凉,棧滿(mǎn)時(shí)再做進(jìn)棧必定產(chǎn)生空間溢出,叫上溢醋安,椩优恚空時(shí)再做退棧也產(chǎn)生空間溢出,稱(chēng)為下溢吓揪。就是分配的內(nèi)存不足以放下數(shù)據(jù)項(xiàng)序列,稱(chēng)為內(nèi)存溢出.
二亲怠、Java 內(nèi)存分配策略
知道了什么是內(nèi)存泄漏,我們就不得不提Java 的內(nèi)存分配策略柠辞。Java程序運(yùn)行時(shí)的內(nèi)存分配策略有三種团秽,分別是靜態(tài)分配,棧式分配,和堆式分配习勤。對(duì)應(yīng)的三種策略使用的內(nèi)存空間是要分別是靜態(tài)存儲(chǔ)區(qū)(也稱(chēng)方法區(qū))踪栋,棧區(qū),和堆區(qū)图毕。
- 靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)夷都,全局static數(shù)據(jù)和常量。這塊內(nèi)存在程序編譯時(shí)就已經(jīng)分配好予颤,并且在程序整個(gè)運(yùn)行期間都存在囤官。
- 棧區(qū):當(dāng)方法執(zhí)行時(shí),方法內(nèi)部的局部變量都建立在棧內(nèi)存中荣瑟,并在方法結(jié)束后自動(dòng)釋放分配的內(nèi)存治拿。因?yàn)闂?nèi)存分配是在處理器的指令集當(dāng)中所以效率很高,但是分配的內(nèi)存容量有限笆焰。
- 堆區(qū):又稱(chēng)動(dòng)態(tài)內(nèi)存分配劫谅,通常就是指在程序運(yùn)行時(shí)直接new出來(lái)的內(nèi)存。這部分內(nèi)存在不適用時(shí)將會(huì)由Java垃圾回收器來(lái)負(fù)責(zé)回收嚷掠。
堆與棧的區(qū)別
在方法體內(nèi)定義的(局部變量)一些基本類(lèi)型的變量和對(duì)象的引用變量都在方法的棧內(nèi)存中分配捏检。
堆內(nèi)存用來(lái)存放所有new出來(lái)的對(duì)象(包括該對(duì)象內(nèi)的所有成員變量)和數(shù)組。
在堆中分配的內(nèi)存不皆,由Java垃圾回收管理器來(lái)自動(dòng)管理贯城。
public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 0;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
如上局部變量s2和mSample2存放在棧內(nèi)存中,mSample3所指向的對(duì)象存放在堆內(nèi)存中霹娄,包括該對(duì)象的成員變量s1和mSample1也存放在堆中能犯,而它自己則存放在棧中( 這句話(huà)的意思就是 Sample mSample3 是在棧中的,而 new Sample()是在堆中的,mSample3就是個(gè)引用變量犬耻,我們可以通過(guò)引用變量訪(fǎng)問(wèn)堆內(nèi)存中的對(duì)象或者數(shù)組 )踩晶。
結(jié)論
局部變量的基本類(lèi)型和引用存儲(chǔ)在棧內(nèi)存中,引用的實(shí)體存儲(chǔ)在堆中枕磁《沈撸——因它們存在于方法中,隨方法的生命周期而結(jié)束计济。
成員變量全部存儲(chǔ)于堆中(包括基本數(shù)據(jù)類(lèi)型茸苇,引用和引用的對(duì)象實(shí)體)÷偌牛——因?yàn)樗鼈儗儆陬?lèi)学密,類(lèi)對(duì)象終究要被new出來(lái)使用。
Java GC 原理與算法機(jī)制
由程序分配內(nèi)存传藏,GC來(lái)釋放內(nèi)存则果。內(nèi)存釋放的原理為該對(duì)象或者數(shù)組不再被引用幔翰,則JVM會(huì)在適當(dāng)?shù)臅r(shí)候回收內(nèi)存。
GC機(jī)制判斷對(duì)象是否存活的算法:
引用計(jì)數(shù)算法
給對(duì)象添加一個(gè)引用計(jì)數(shù)器西壮,當(dāng)有一個(gè)地方引用它時(shí)遗增,計(jì)數(shù)器值就加1;當(dāng)引用失效時(shí)款青,計(jì)數(shù)器就減1做修;任何時(shí)候計(jì)數(shù)器都為0的對(duì)象就是不可能再被使用的。引用計(jì)數(shù)器算法(Reference Counting)實(shí)現(xiàn)簡(jiǎn)單抡草,判定效率也很高饰及,在大部分情況下他都是一個(gè)不錯(cuò)的算法。但是康震,Java語(yǔ)言中沒(méi)有選用引用計(jì)數(shù)算法來(lái)管理內(nèi)存燎含,其中最主要的原因是他很難解決對(duì)象之間的互相循環(huán)引用的問(wèn)題。根搜索算法(GC Roots Tracing)
基本思路是通過(guò)一系列名為“GC Roots”的對(duì)象作為起始點(diǎn)腿短,從這個(gè)節(jié)點(diǎn)開(kāi)始向下搜索屏箍,搜索所走過(guò)的路徑稱(chēng)為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連(用圖論術(shù)語(yǔ)描述就是從GC Roots到這個(gè)對(duì)象不可達(dá))時(shí),則證明此對(duì)象是不可用的橘忱。在主流的商用程序語(yǔ)言中(Java赴魁、C#),都是使用根搜索算法判定對(duì)象是否存活的。
常見(jiàn)回收算法:
-
標(biāo)記-清除算法
:標(biāo)記階段:先通過(guò)根節(jié)點(diǎn)钝诚,標(biāo)記所有從根節(jié)點(diǎn)開(kāi)始的可達(dá)對(duì)象颖御。因此,未被標(biāo)記的對(duì)象就是未被引用的垃圾對(duì)象凝颇;清除階段:清除所有未被標(biāo)記的對(duì)象潘拱。 -
復(fù)制算法
:(新生代的GC)將原有的內(nèi)存空間分為兩塊,每次只使用其中一塊拧略,在垃圾回收時(shí)泽铛,將正在使用的內(nèi)存中的存活對(duì)象復(fù)制到未使用的內(nèi)存塊中,然后清除正在使用的內(nèi)存塊中的所有對(duì)象辑鲤。 -
標(biāo)記-整理算法
:(老年代的GC)標(biāo)記階段:先通過(guò)根節(jié)點(diǎn),標(biāo)記所有從根節(jié)點(diǎn)開(kāi)始的可達(dá)對(duì)象杠茬。因此月褥,未被標(biāo)記的對(duì)象就是未被引用的垃圾對(duì)象整理階段:將將所有的存活對(duì)象壓縮到內(nèi)存的一端;之后瓢喉,清理邊界外所有的空間 -
分代收集算法
:存活率低:少量對(duì)象存活宁赤,適合復(fù)制算法:在新生代中,每次GC時(shí)都發(fā)現(xiàn)有大批對(duì)象死去栓票,只有少量存活(新生代中98%的對(duì)象都是“朝生夕死”)决左,那就選用復(fù)制算法愕够,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成GC。存活率高:大量對(duì)象存活佛猛,適合用標(biāo)記-清理/標(biāo)記-整理:在老年代中惑芭,因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)他進(jìn)行分配擔(dān)保继找,就必須使用“標(biāo)記-清理”/“標(biāo)記-整理”算法進(jìn)行GC遂跟。
三、導(dǎo)致內(nèi)存泄漏的原因
一句話(huà):長(zhǎng)生命周期的對(duì)象持有短生命周期的對(duì)象婴渡。短生命周期對(duì)象無(wú)法及時(shí)釋放幻锁。
導(dǎo)致內(nèi)存泄漏主要的原因是,先前申請(qǐng)了內(nèi)存空間而忘記了釋放边臼。如果程序中存在對(duì)無(wú)用對(duì)象的引用哄尔,那么這些對(duì)象就會(huì)駐留內(nèi)存,消耗內(nèi)存柠并,因?yàn)闊o(wú)法讓垃圾回收器GC驗(yàn)證這些對(duì)象是否不再需要岭接。如果存在對(duì)象的引用,這個(gè)對(duì)象就被定義為"有效的活動(dòng)"堂鲤,同時(shí)不會(huì)被釋放亿傅。要確定對(duì)象所占內(nèi)存將被回收,我們就要?jiǎng)?wù)必確認(rèn)該對(duì)象不再會(huì)被使用瘟栖。典型的做法就是把對(duì)象數(shù)據(jù)成員設(shè)為null或者從集合中移除該對(duì)象葵擎。但當(dāng)局部變量不需要時(shí),不需明顯的設(shè)為null半哟,因?yàn)橐粋€(gè)方法執(zhí)行完畢時(shí)酬滤,這些引用會(huì)自動(dòng)被清理。
-
static變量引用待釋放類(lèi)實(shí)例
比如MainActivity里面有一個(gè)靜態(tài)的變量sMainActivity = this,這就會(huì)照成內(nèi)存泄漏寓涨,比如靜態(tài)集合類(lèi)沒(méi)有及時(shí)setnull - 單例
- 如果此時(shí)傳入的是 Application 的 Context盯串,因?yàn)?Application 的生命周期就是整個(gè)應(yīng)用的生命周期,所以這將沒(méi)有任何問(wèn)題戒良。
- 如果此時(shí)傳入的是 Activity 的 Context体捏,當(dāng)這個(gè) Context 所對(duì)應(yīng)的 Activity 退出時(shí),由于該 Context 的引用被單例對(duì)象所持有糯崎,其生命周期等于整個(gè)應(yīng)用程序的生命周期几缭,所以當(dāng)前 Activity 退出時(shí)它的內(nèi)存并不會(huì)被回收,這就造成泄漏了沃呢。
-
資源對(duì)象沒(méi)有關(guān)閉
廣播沒(méi)有反注冊(cè)年栓,EventBus沒(méi)有反注冊(cè),Cursor對(duì)象沒(méi)有關(guān)薄霜,流沒(méi)有關(guān) 等等 - 集合類(lèi)泄漏
List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object o = new Object();
objectList.add(o);
o = null;
}
上面的實(shí)例某抓,雖然在循環(huán)中把引用o釋放了纸兔,但是它被添加到了objectList中,所以objectList也持有對(duì)象的引用否副,此時(shí)該對(duì)象是無(wú)法被GC的汉矿。因此對(duì)象如果添加到集合中,還必須從中刪除副编,最簡(jiǎn)單的方法
//釋放objectList
objectList.clear();
objectList=null;
-
webview內(nèi)存泄漏
webview是開(kāi)啟的一個(gè)子線(xiàn)程负甸,如果子線(xiàn)程沒(méi)有運(yùn)行結(jié)束就銷(xiāo)毀了activity的話(huà),那么子線(xiàn)程還是持有activity的痹届,這時(shí)候就產(chǎn)生了內(nèi)存泄漏 -
Handler使用非靜態(tài)的時(shí)候
我們知道非static的內(nèi)部類(lèi)會(huì)持有外部類(lèi)的引用呻待,如果此時(shí)Handler從消息隊(duì)列有未處理的Message,在Activity finish之后Message還是存在队腐,那么Handler也存在蚕捉,handler里面又有Context的引用,也就是Activity也存在柴淘,那就發(fā)生了內(nèi)存泄漏迫淹。
解決辦法:靜態(tài)內(nèi)部類(lèi),即使用static修飾Handler为严,通過(guò)WeakReference
來(lái)保持外部的Activity對(duì)象敛熬。
private Handler mHandler = new MyHandler(this);
private static class MyHandler extends Handler{
private final WeakReference<Activity> mActivity;
public MyHandler(Activity activity) {
mActivity = new WeakReference<Activity>(activity);
}
@Override
public void handleMessage(Message msg) {
System.out.println(msg);
if(mActivity.get() == null) {
return;
}
}
}
四、解決內(nèi)存泄漏的途徑
下面演示幾個(gè)使用LeakCanary解決內(nèi)存泄漏的例子
1第股、消滅Context泄漏
泄漏原因:將Activity當(dāng)做Context對(duì)象使用
這就是一條內(nèi)存泄漏過(guò)程的引用鏈应民,這條引用鏈最后一行的位置是泄漏發(fā)生的地方,第一行是泄漏的源頭夕吻,我們主要觀(guān)察的地方就是內(nèi)存泄漏的源頭部分诲锹。比如,上面的內(nèi)存泄漏的源頭就是MainActivity中使用了MainActivity這個(gè)Activity作為Context所引起的內(nèi)存泄漏涉馅。
解決方式就是使用ApplicationContext代替Activity Context
2归园、消滅static引用
可以看到在代碼中使用了static修飾的變量dialog,static關(guān)鍵字修飾的dialog屬于類(lèi)所有稚矿,而不屬于任何特定的實(shí)例庸诱,所以即使UploadDataDialog銷(xiāo)毀了,dialog還是存在的晤揣。所以在Dialog銷(xiāo)毀的時(shí)候這個(gè)static變量不能被GC回收桥爽,所以我們要重寫(xiě)dismiss方法,當(dāng)UploadDataDialog dismiss的時(shí)候我們就讓其將成員變量dialog置為空即可碉渡。
public class UploadDataDialog extends Dialog{
private Context context;
private static UploadDataDialog dialog;
private ImageView ivProgress;
public UploadDataDialog(Context context) {
super(context);
this.context = context;
}
public UploadDataDialog(Context context, int themeResId) {
super(context, themeResId);
this.context = context;
}
public static UploadDataDialog showDialog(Context context){
dialog = new UploadDataDialog(context, R.style.UploadDataDialog);
dialog.setContentView(R.layout.dialog_uploaddata);
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus && dialog != null){
ivProgress = (ImageView) dialog.findViewById(R.id.ivProgress);
Animation animation = AnimationUtils.loadAnimation(context, R.anim.dialog_progress_anim);
ivProgress.startAnimation(animation);
}
}
@Override
public void dismiss() {
super.dismiss();
if (dialog != null) {
dialog = null;
}
}
}
3、使用暴力反射解決內(nèi)存泄漏問(wèn)題
這個(gè)問(wèn)題還是比較奇怪的母剥,我使用LocalBroadcastManager管理各種監(jiān)聽(tīng)的注冊(cè)反注冊(cè)滞诺,但是現(xiàn)在提示我LocalBroadcastManager中的mInstance出現(xiàn)了泄漏形导,而LocalBroadcastManager是Android SDK中的,所以只能通過(guò)暴力反射來(lái)拿到這個(gè)成員變量然后置空习霹。
@Override
protected void onDestroy() {
super.onDestroy();
try {
Class clazz = Class.forName("android.support.v4.content.LocalBroadcastManager");
//獲取成員屬性mInstance(單例對(duì)象)
Field field = clazz.getDeclaredField("mInstance");
//設(shè)置private屬性可訪(fǎng)問(wèn)
if (field.isAccessible() == false) {
field.setAccessible(true);
}
//置空屬性
field.set(null, null);
} catch (Exception e) {
e.printStackTrace();
}
}
4朵耕、一個(gè)java內(nèi)存泄漏的例子
class MyList{
/*
* 此處只為掩飾效果,并沒(méi)有進(jìn)行封裝之類(lèi)的操作
*
* 將List集合用關(guān)鍵字 static 聲明淋叶,這時(shí)這個(gè)集合將不屬于任何 MyList 對(duì)象阎曹,而是一個(gè)類(lèi)成員變量
*
*/
public static List<String> list = new ArrayList<String>();
}
class Demmo{
public static void main(String[] args) {
MyList list = new MyList();
list.list.add("123456");
// 此時(shí)即便我們將 list指向null,仍然存在內(nèi)存泄漏煞檩,因?yàn)镸yList中的list是靜態(tài)的处嫌,它屬于類(lèi)所有而不屬于任何特定的實(shí)例
list = null;
}
}
五、LeakCanary解決內(nèi)存泄露的原理
LeakCanary 在 Application 中安裝完成后斟湃,會(huì)注冊(cè)對(duì)應(yīng)用內(nèi)所有 Activity 生命周期的監(jiān)聽(tīng)熏迹,也就是在Activity的onDestroy的時(shí)候?qū)ctivity傳到watch方法中,然后在watch方法中的時(shí)候會(huì)把a(bǔ)ctivity包裝成一個(gè)弱引用凝赛。并且為這個(gè)弱引用指定了一個(gè)隊(duì)列注暗,這個(gè)隊(duì)列的作用就是當(dāng)發(fā)生GC后,弱引用就會(huì)被放到這個(gè)隊(duì)列里面墓猎。(Activity如果被強(qiáng)引用的話(huà)捆昏,就不會(huì)放到這個(gè)隊(duì)列里面了,也就是內(nèi)存泄漏了毙沾。)所以我們就可以通過(guò)手動(dòng)執(zhí)行一下GC操作骗卜,來(lái)看隊(duì)列里面是不是包含這個(gè)activity對(duì)象,如果不包含就是內(nèi)存泄漏搀军。