Android之內(nèi)存溢出和內(nèi)存泄漏的原因和解決方案

基礎(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)存泄漏奈应。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末澜掩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子杖挣,更是在濱河造成了極大的恐慌肩榕,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異株汉,居然都是意外死亡筐乳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門乔妈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝙云,“玉大人,你說我怎么就攤上這事路召〔伲” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵股淡,是天一觀的道長身隐。 經(jīng)常有香客問我,道長唯灵,這世上最難降的妖魔是什么贾铝? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮埠帕,結(jié)果婚禮上垢揩,老公的妹妹穿的比我還像新娘。我一直安慰自己敛瓷,他們只是感情好水孩,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著琐驴,像睡著了一般俘种。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绝淡,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天宙刘,我揣著相機與錄音,去河邊找鬼牢酵。 笑死悬包,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的馍乙。 我是一名探鬼主播布近,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼丝格!你這毒婦竟也來了撑瞧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤显蝌,失蹤者是張志新(化名)和其女友劉穎预伺,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡酬诀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年脏嚷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞒御。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡父叙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肴裙,到底是詐尸還是另有隱情高每,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布践宴,位于F島的核電站鲸匿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏阻肩。R本人自食惡果不足惜带欢,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烤惊。 院中可真熱鬧乔煞,春花似錦、人聲如沸柒室。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雄右。三九已至空骚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間擂仍,已是汗流浹背囤屹。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逢渔,地道東北人肋坚。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像肃廓,于是被迫代替她去往敵國和親智厌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 前言 之前研究過一段時間關(guān)于 Android 內(nèi)存泄漏的知識盲赊,大致了解了導(dǎo)致內(nèi)存泄漏的一些原因铣鹏,但是沒有深入去探究...
    Zackratos閱讀 19,193評論 19 49
  • Android 內(nèi)存管理的目的 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。簡單粗...
    晨光光閱讀 1,292評論 1 4
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題角钩。內(nèi)存泄漏...
    _痞子閱讀 1,633評論 0 8
  • 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題吝沫。內(nèi)存泄漏大家都不陌生了,簡單粗俗的講递礼,...
    宇宙只有巴掌大閱讀 2,362評論 0 12
  • 今天是E戰(zhàn)到底特訓(xùn)營第四期最后一次打卡了惨险,How times fly! 21天轉(zhuǎn)瞬即逝,回顧這段時間脊髓,一個字“充實...
    重陽2308閱讀 194評論 0 0