基礎(chǔ)
JAVA是在JVM所虛擬出的內(nèi)存環(huán)境中運行的懂鸵,內(nèi)存分為三個區(qū):堆、棧和方法區(qū)父腕。
- 棧(stack):是簡單的數(shù)據(jù)結(jié)構(gòu),程序運行時系統(tǒng)自動分配青瀑,使用完畢后自動釋放璧亮。優(yōu)點:速度快。
- 堆(heap):用于存放由new創(chuàng)建的對象和數(shù)組斥难。在堆中分配的內(nèi)存枝嘶,一方面由java虛擬機自動垃圾回收器來管理,另一方面還需要程序員提供修養(yǎng)哑诊,防止內(nèi)存泄露問題群扶。
- 方法區(qū)(method):又叫靜態(tài)區(qū),跟堆一樣镀裤,被所有的線程共享竞阐。方法區(qū)包含所有的class和static變量。
概念
內(nèi)存溢出(Out of Memory):系統(tǒng)會給每個APP分配內(nèi)存也就是Heap Size值暑劝。當(dāng)APP占用的內(nèi)存加上我們申請的內(nèi)存資源超過了Dalvik虛擬機的最大內(nèi)存時就會拋出的Out Of Memory異常骆莹。
內(nèi)存泄漏(Memory Leak):當(dāng)一個對象不在使用了,本應(yīng)該被垃圾回收器(JVM)回收担猛。但是這個對象由于被其他正在使用的對象所持有汪疮,造成無法被回收的結(jié)果峭火。內(nèi)存泄漏最終會導(dǎo)致內(nèi)存溢出。
內(nèi)存抖動:內(nèi)存抖動是指在短時間內(nèi)有大量的對象被創(chuàng)建或者被回收的現(xiàn)象智嚷,主要是循環(huán)中大量創(chuàng)建卖丸、回收對象。這種情況應(yīng)當(dāng)盡量避免盏道。
它們?nèi)叩闹匾燃壏謩e:內(nèi)存溢出 > 內(nèi)存泄露 > 內(nèi)存抖動稍浆。
內(nèi)存溢出對我們的App來說,影響是非常大的猜嘱。有可能導(dǎo)致程序閃退衅枫,無響應(yīng)等現(xiàn)象,因此朗伶,我們一定要優(yōu)先解決OOM的問題弦撩。強引用:強引用是使用最普遍的引用。如果一個對象具有強引用论皆,那垃圾回收器絕不會回收它益楼。 當(dāng)內(nèi)存空間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤点晴,使程序異常終止感凤,也不會靠隨意回收具有強引用的對象來解決內(nèi)存不足的問題。
軟引用:如果一個對象只具有軟引用粒督,但內(nèi)存空間足夠時陪竿,垃圾回收器就不會回收它;直到虛擬機報告內(nèi)存不夠時才會回收, 只要垃圾回收器沒有回收它屠橄,該對象就可以被程序使用族跛。軟引用可用來實現(xiàn)內(nèi)存敏感的高速緩存。 軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用锐墙,如果軟引用所引用的對象被垃圾回收器回收庸蔼,Java虛擬機就會把這個軟引用加入到與之關(guān)聯(lián)的引用隊列中。
弱引用:只具有弱引用的對象擁有更短暫的生命周期贮匕。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過程中姐仅,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當(dāng)前內(nèi)存空間是否足夠刻盐,都會回收它的內(nèi)存掏膏。 不過,由于垃圾回收器是一個優(yōu)先級很低的線程敦锌,因此不一定會很快發(fā)現(xiàn)那些只具有弱引用的對象馒疹。 弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對象被垃圾回收乙墙,Java虛擬機就會把這個弱引用加入到與之關(guān)聯(lián)的引用隊列中颖变。
虛引用:虛引用可以理解為虛設(shè)的引用生均,與其他幾種引用都不同,虛引用并不會決定對象的生命周期腥刹。如果一個對象僅持有虛引用马胧,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收衔峰。 虛引用主要用來跟蹤對象被垃圾回收器回收的活動佩脊。
虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列 (ReferenceQueue)聯(lián)合使用。 當(dāng)垃圾回收器準(zhǔn)備回收一個對象時垫卤,如果發(fā)現(xiàn)它還有虛引用威彰,就會在回收對象的內(nèi)存之前,把這個虛引用加入到與之 關(guān)聯(lián)的引用隊列中穴肘。 程序可以通過判斷引用隊列中是否已經(jīng)加入了虛引用歇盼,來了解被引用的對象是否將要被垃圾回收。 如果程序發(fā)現(xiàn)某個虛引用已經(jīng)被加入到引用隊列评抚,那么就可以在所引用的對象的內(nèi)存被回收之前采取必要的行動豹缀。
關(guān)系
內(nèi)存泄漏是造成應(yīng)用程序OOM的主要原因之一。由于Android系統(tǒng)為每個應(yīng)用程序分配的內(nèi)存有限盈咳,當(dāng)一個應(yīng)用中產(chǎn)生的內(nèi)存泄漏比較多時,就難免會導(dǎo)致應(yīng)用所需要的內(nèi)存超過這個系統(tǒng)分配的內(nèi)存限額边翼,這就造成了內(nèi)存溢出而導(dǎo)致應(yīng)用Crash鱼响。
我們的App多次出現(xiàn)內(nèi)存泄露,可能就會導(dǎo)致內(nèi)存溢出组底。但是丈积,我們的App出現(xiàn)內(nèi)存溢出,不一定就是因為內(nèi)存泄露债鸡,因為本身Android系統(tǒng)分配給每一個的App的空間就是那么一點江滨。另外,內(nèi)存泄露也不一定就會出現(xiàn)內(nèi)存溢出厌均,因為還是泄露的速度比較慢唬滑,系統(tǒng)將進程殺死了,也就不會內(nèi)存溢出棺弊。不過晶密,發(fā)現(xiàn)內(nèi)存泄露,我們還是要第一時間解決模她。
危害
- 內(nèi)存溢出:會觸發(fā)Java.lang.OutOfMemoryError稻艰,造成程序崩潰。
- 內(nèi)存泄漏:過多的內(nèi)存泄漏會造成內(nèi)存溢出侈净,同樣也會造成相關(guān)UI的卡頓現(xiàn)象尊勿。
判斷是否有內(nèi)存泄露的工具
LeackCanary
Memory Monitor
DDMS
處理方式匯總
強引用僧凤,軟引用和弱引用
- 釋放強引用,使用軟引用和弱引用元扔;
大量的圖片躯保、音頻、視頻處理摇展,當(dāng)在內(nèi)存比較低的系統(tǒng)上也容易造成內(nèi)存溢出
- 建議使用第三方吻氧,或者JNI來進行處理;
Bitmap對象的處理
不要在主線程中處理圖片
-
使用Bitmap對象要用recycle釋放
// Bitmap對象沒有被回收 if (!bitmapObject.isRecyled()) { // 釋放 bitmapObject.recycle(); // 提醒系統(tǒng)及時回收 System.gc(); }
控制圖片的大小咏连,壓縮大圖盯孙,高效處理,加載合適屬性的圖片祟滴。
當(dāng)我們有些場景是可以顯示縮略圖的時候振惰,就不要調(diào)用網(wǎng)絡(luò)請求加載大圖,例如在RecyclerView中垄懂,我們在上下滑動的時候骑晶,就不要去調(diào)用網(wǎng)絡(luò)請求,當(dāng)監(jiān)聽到滑動結(jié)束的時候草慧,才去加載大圖桶蛔,以免上下滑動的時候產(chǎn)生卡頓現(xiàn)象。
非靜態(tài)內(nèi)部類和匿名內(nèi)部類Handler漫谷、Thread仔雷、Runnable等由于持有外部類Activity的引用,從而關(guān)閉activity舔示,線程未完成造成內(nèi)存泄漏
在Activity中創(chuàng)建非靜態(tài)內(nèi)部類碟婆,非靜態(tài)內(nèi)部類會持有Activity的隱式引用,若內(nèi)部類生命周期長于Activity惕稻,會導(dǎo)致Activity實例無法被回收竖共。(屏幕旋轉(zhuǎn)后會重新創(chuàng)建Activity實例,如果內(nèi)部類持有引用俺祠,將會導(dǎo)致旋轉(zhuǎn)前的實例無法被回收)公给。
-
如果一定要使用內(nèi)部類,就改用static內(nèi)部類蜘渣,在內(nèi)部類中通過WeakReference的方式引用外界資源妓布。對Handler、Thread宋梧、Runnable等使用弱引用匣沼,并且調(diào)用removeCallbacksAndMessages等移除。
舉例:在下面這段代碼中存在一個非靜態(tài)的匿名類對象Thread捂龄,會隱式持有一個外部類的引用MainActivity 释涛。同理加叁,若是這個Thread作為MainActivity的內(nèi)部類而不是匿名內(nèi)部類,他同樣會持有外部類的引用唇撬。public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); leakFun(); } private void leakFun() { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }
在線程休眠的這10s內(nèi)它匕,會一直隱式持有外部類的引用MainActivity,如果在10s之前窖认,銷毀MainActivity豫柬,就會報內(nèi)存泄漏。同理扑浸,若是這個Thread作為MainActivity的內(nèi)部類而不是匿名內(nèi)部類烧给,也會內(nèi)存泄漏。
總而言之:如果Activity在銷毀之前喝噪,任務(wù)還未完成础嫡, 那么將導(dǎo)致Activity的內(nèi)存資源無法回收,造成內(nèi)存泄漏酝惧。
解決辦法:在這里只需要將為Thread匿名類定義成靜態(tài)的內(nèi)部類即可(靜態(tài)的內(nèi)部類不會持有外部類的一個隱式引用)榴鼎。或保證在Activity在銷毀之前晚唇,完成任務(wù)巫财!
- 在關(guān)閉Activity的時候停掉你的后臺線程。線程停掉了哩陕,就相當(dāng)于切斷了Handler和外部連接的線平项,Activity自然會在合適的時候被回收。
資源未及時關(guān)閉造成的內(nèi)存泄漏
對于使用了BraodcastReceiver萌踱,ContentObserver葵礼,Cursor号阿,F(xiàn)ile并鸵,Stream,ContentProvider扔涧,Bitmap园担,動畫,I/O枯夜,數(shù)據(jù)庫弯汰,網(wǎng)絡(luò)的連接等資源的使用,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷湖雹,否則這些資源將不會被回收咏闪,造成內(nèi)存泄漏。
廣播BraodcastReceiver:記得注銷注冊unregisterReceiver();
文件流File:記得關(guān)閉流InputStream / OutputStream.close();
數(shù)據(jù)庫游標(biāo)Cursor:使用后關(guān)閉游標(biāo)cursor.close();
對于圖片資源Bitmap:當(dāng)它不再被使用時摔吏,應(yīng)調(diào)用recycle()回收此對象的像素所占用的內(nèi)存鸽嫂,再賦為null
-
動畫:屬性動畫或循環(huán)動畫纵装,在Activity退出時需要停止動畫。在屬性動畫中有一類無限循環(huán)動畫据某,如果在Activity中播放這類動畫并且在onDestroy中沒有去停止動畫橡娄,那么這個動畫將會一直播放下去,這時候Activity會被View所持有癣籽,從而導(dǎo)致Activity無法被釋放挽唉。在Activity中onDestroy去調(diào)用objectAnimator.cancel()來停止動畫。
public class LeakActivity extends AppCompatActivity { private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_leak); textView = (TextView) findViewById(R.id.text_view); ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView筷狼, "rotation"瓶籽, 0, 360); objectAnimator.setRepeatCount(ValueAnimator.INFINITE); objectAnimator.start(); } }
-
集合對象及時清理桑逝,使得JVM回收:我們通常會把對象存入集合中棘劣,當(dāng)不使用時,清空集合楞遏,讓相關(guān)對象不再被引用;
objectList.clear(); objectList=null;
注銷監(jiān)聽器
- 在onPause()/onDestroy()方法中解除監(jiān)聽器茬暇,包括在Android自己的Listener,Location Service或Display Manager Service以及自己寫的Listener寡喝。
static關(guān)鍵字修飾的變量由于生命周期過長糙俗,容易造成內(nèi)存泄漏
static對象的生命周期過長,應(yīng)該謹(jǐn)慎使用预鬓。一定要使用要及時進行null處理巧骚。
-
靜態(tài)變量Activity和View會導(dǎo)致內(nèi)存泄漏。例如:context格二,textView實例的生命周期與應(yīng)用的生命周期一樣劈彪,而他們都持有當(dāng)前Activity的(MainActivity )引用,一旦MainActivity 銷毀顶猜,而他的引用一直被持有沧奴,就不會被回收。所以长窄,內(nèi)存泄漏就產(chǎn)出了滔吠。
public class MainActivity extends AppCompatActivity{ private static Context context; private static TextView textView; @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); context = this; textView = new TextView(this); } }
如果使用Context ,盡可能使用Applicaiton的Context
單例模式造成的內(nèi)存泄漏挠日,如context的使用疮绷,單例中傳入的是activity的context,在關(guān)閉activity時嚣潜,activity的內(nèi)存無法被回收冬骚,因為單例持有activity的引用。
在context的使用上,應(yīng)該傳入application的context到單例模式中只冻,這樣就保證了單例的生命周期跟application的生命周期一樣夜涕。
因為單例的靜態(tài)特性使得單例的生命周期和應(yīng)用的生命周期一樣長,這就說明了如果一個對象已經(jīng)不需要使用了属愤,而單例對象還持有該對象的引用女器,那么這個對象將不能被正常回收住诸,這就導(dǎo)致了內(nèi)存泄漏驾胆。
-
單例模式應(yīng)該盡量少持有生命周期不同的外部對象,一旦持有該對象的時候贱呐,必須在該對象的生命周期結(jié)束前null
public class TestManager { private static TestManager instance; private Context context; private TestManager(Context context) { this.context = context; } public static TestManager getInstance(Context context) { if (instance != null) { instance = new TestManager(context); } return instance; } }
這是一個普通的單例模式丧诺,當(dāng)創(chuàng)建這個單例的時候,由于需要傳入一個Context奄薇,所以這個Context的生命周期的長短至關(guān)重要:
1驳阎、傳入的是Application的Context:這將沒有任何問題,因為單例的生命周期和Application的一樣長 ;
2馁蒂、傳入的是Activity的Context:當(dāng)這個Context所對應(yīng)的Activity退出時呵晚,由于該Context和Activity的生命周期一樣長(Activity間接繼承于Context),所以當(dāng)前Activity退出時它的內(nèi)存并不會被回收沫屡,因為單例對象持有該Activity的引用饵隙。
所以正確的單例應(yīng)該修改為下面這種方式:
public class TestManager {
private static TestManager instance;
private Context context;
private TestManager(Context context) {
this.context = context.getApplicationContext();
}
public static TestManager getInstance(Context context) {
if (instance != null) {
instance = new TestManager(context);
}
return instance;
}
}
這樣不管傳入什么Context最終將使用Application的Context,而單例的生命周期和應(yīng)用的一樣長沮脖,這樣就防止了內(nèi)存泄漏金矛。
不要使用String進行字符串拼接
- 嚴(yán)格的講,String拼接只能歸結(jié)到內(nèi)存抖動中勺届,因為產(chǎn)生的String副本能夠被GC驶俊,不會造成內(nèi)存泄露。
- 頻繁的字符串拼接免姿,使用StringBuffer或者StringBuilder代替String饼酿,可以在一定程度上避免OOM和內(nèi)存抖動。
三方庫
- 對于EventBus养泡,RxJava等一些第三方開源框架的使用嗜湃,若是在Activity銷毀之前沒有進行解除訂閱將會導(dǎo)致內(nèi)存泄漏奈应。