深入Android內(nèi)存泄露

深入內(nèi)存泄露

android應(yīng)用層的內(nèi)存泄露,其實(shí)就是java虛擬機(jī)的內(nèi)存泄漏.
(這里,暫不討論C/C++本地內(nèi)存的堆泄漏)


1.知識(shí)儲(chǔ)備

1.Java內(nèi)存模型


相關(guān)內(nèi)存對(duì)象模型,參照博客精講Java內(nèi)存模型

  1. 寄存器(register)。這是最快的保存區(qū)域,這是主要由于它位于處理器內(nèi)部。然而,寄存器的數(shù)量十分有限,所以寄存器是需要由編譯器分配的。我們對(duì)此沒(méi)有直接的控制權(quán),也不可能在自己的程序里找到寄存器存在的任何蹤跡。

(2) 堆棧(stack)在執(zhí)行函數(shù)(方法)時(shí),函數(shù)一些內(nèi)部變量的存儲(chǔ)都可以放在棧上面創(chuàng)建,函數(shù)執(zhí)行結(jié)束的時(shí)候這些存儲(chǔ)單元就會(huì)自動(dòng)被釋放掉。位于通用RAM(隨機(jī)訪問(wèn)存儲(chǔ)器)中。可通過(guò)它的“堆棧指針” 獲得處理的直接支持。堆棧指針若向下移,會(huì)創(chuàng)建新的內(nèi)存;若向上移,則會(huì)釋放那些內(nèi)存。這是一種特別快、特別有效的數(shù)據(jù)保存方式,僅次于寄存器。

(3) 堆(heap)。一種通用性的內(nèi)存池(也在RAM區(qū)域),堆是不連續(xù)的內(nèi)存區(qū)域,堆空間比較靈活也特別大。其中保存了Java對(duì)象(<font color=#FF4500>對(duì)象里面的成員變量也在其中</font>)。在堆里分配存儲(chǔ)空間時(shí)會(huì)花掉更長(zhǎng)的時(shí)間!也叫做動(dòng)態(tài)內(nèi)存分配。

(4) 靜態(tài)存儲(chǔ)(static storage)。這兒的“靜態(tài)”(Static)是指“位于固定位置”(盡管也在RAM 里)。程序運(yùn)行期間,靜態(tài)存儲(chǔ)的數(shù)據(jù)將隨時(shí)等候調(diào)用。可用static關(guān)鍵字指出一個(gè)對(duì)象的特定元素是靜態(tài)的。但Java 對(duì)象本身永遠(yuǎn)都不會(huì)置入靜態(tài)存儲(chǔ)空間,隨著JVM的生命周期結(jié)束而結(jié)束,即當(dāng)app完全退出,他才會(huì)釋放

(5) 常數(shù)存儲(chǔ)(constant storage)。常數(shù)值通常直接置于程序代碼內(nèi)部。這樣做是安全的,因?yàn)樗鼈冇肋h(yuǎn)都不會(huì)改變。

(6) 非RAM 存儲(chǔ)(non-storage-RAM)。若數(shù)據(jù)完全獨(dú)立于一個(gè)程序之外,則程序不運(yùn)行時(shí)仍可存在,并在程序的控制范圍之外。其中兩個(gè)最主要的例子便是“ 流式對(duì)象”和“固定對(duì)象” 。對(duì)于流式對(duì)象,對(duì)象會(huì)變成字節(jié)流,通常會(huì)發(fā)給另一臺(tái)機(jī)器。而對(duì)于固定對(duì)象,對(duì)象保存在磁盤(pán)中。

2.GC回收機(jī)制

引用自http://blog.csdn.net/jiafu1115/article/details/7024323

首先JVM是對(duì)堆進(jìn)行回收操作.

1.JVM堆中分類(lèi)

(1) 新域young generation:存儲(chǔ)所有新成生的對(duì)象

(2) 舊域old generation:新域中的對(duì)象,經(jīng)過(guò)了一定次數(shù)的GC循環(huán)后,被移入舊域

(3) 永久域PermanentGeneration:存儲(chǔ)類(lèi)和方法對(duì)象,從配置的角度看,這個(gè)域是獨(dú)立的,不包括在JVM堆內(nèi)。默認(rèn)為4M。

2.Gc回收流程

1.當(dāng)eden滿了,觸發(fā)young GC;

2.young GC做2件事:一,去掉一部分沒(méi)用的object;二,把老的還被引用的object發(fā)到survior里面,等下幾次GC以后,survivor再放到old里面。

3.當(dāng)old滿了,觸發(fā)full GC。full GC很消耗內(nèi)存,把old,young里面大部分垃圾回收掉。這個(gè)時(shí)候用戶線程都會(huì)被block。

3.Gc回收總結(jié)

1.JVM堆的大小決定了GC的運(yùn)行時(shí)間。如果JVM堆的大小超過(guò)一定的限度,那么GC的運(yùn)行時(shí)間會(huì)很長(zhǎng)。

2.對(duì)象生存的時(shí)間越長(zhǎng),GC需要的回收時(shí)間也越長(zhǎng),影響了回收速度。

3.大多數(shù)對(duì)象都是短命的,所以,如果能讓這些對(duì)象的生存期在GC的一次運(yùn)行周期內(nèi),wonderful!

4.應(yīng)用程序中,建立與釋放對(duì)象的速度決定了垃圾收集的頻率。

5.如果GC一次運(yùn)行周期超過(guò)3-5秒,這會(huì)很影響應(yīng)用程序的運(yùn)行,如果可以,應(yīng)該減少JVM堆的大小了。

6.前輩經(jīng)驗(yàn)之談:通常情況下,JVM堆的大小應(yīng)為物理內(nèi)存的80%。

3.內(nèi)存抖動(dòng)

內(nèi)存抖動(dòng)這個(gè)術(shù)語(yǔ)可用于描述在極短時(shí)間內(nèi)分配給對(duì)象的過(guò)程.

例如,當(dāng)你在循環(huán)語(yǔ)句中配置一系列臨時(shí)對(duì)象,或者在繪圖功能中配置大量對(duì)象時(shí),這相當(dāng)于內(nèi)循環(huán),當(dāng)屏幕需要重新繪制或出現(xiàn)動(dòng)畫(huà)時(shí),你需要一幀幀使用這些功能,不過(guò)它會(huì)迅速增加你的堆的壓力。

Memory Monitor 內(nèi)存抖動(dòng)圖例:


2.內(nèi)存泄漏對(duì)程序造成的影響

1.直接:消耗內(nèi)存,造成系應(yīng)用OutOfMemory.

一個(gè)android應(yīng)用程序,其實(shí)就是一個(gè)jvm虛擬機(jī)實(shí)例,而一個(gè)jvm的實(shí)例,在初始的時(shí)候,大小不等 16M,32M,64M(根據(jù)手機(jī)廠商和版本不同而不同),當(dāng)然大小也可以修改,參考修改博客

2.間接:gc回收頻繁 造成應(yīng)用卡頓ANR.

GC回收時(shí)間過(guò)長(zhǎng)導(dǎo)致卡頓

首先,當(dāng)內(nèi)存不足的時(shí)候,gc會(huì)主動(dòng)回收沒(méi)用的內(nèi)存.但是,內(nèi)存回收也是需要時(shí)間的.

上圖中,android在畫(huà)圖(播放視頻等)的時(shí)候,draw到界面的對(duì)象,和gc回收垃圾資源之間高頻率交替的執(zhí)行.就會(huì)產(chǎn)生內(nèi)存抖動(dòng).

很多數(shù)據(jù)就會(huì)污染內(nèi)存堆,馬上就會(huì)有許多GCs啟動(dòng),由于這一額外的內(nèi)存壓力,也會(huì)產(chǎn)生突然增加的運(yùn)算造成卡頓現(xiàn)象

任何線程的任何操作都會(huì)需要暫停,等待GC操作完成之后,其他操作才能夠繼續(xù)運(yùn)行,所以垃圾回收運(yùn)行的次數(shù)越少,對(duì)性能的影響就越少


3.內(nèi)存泄露的原因

內(nèi)存泄漏的本質(zhì):不再用到的對(duì)象,被錯(cuò)誤引用,而無(wú)法被回收

未引用對(duì)象可以被垃圾回收機(jī)制回收,而被引用對(duì)象不能被垃圾回收機(jī)制回收。
當(dāng)內(nèi)存不足,gc會(huì)回收垃圾內(nèi)存
垃圾內(nèi)存是 沒(méi)有別人使用的內(nèi)存,好的內(nèi)存

內(nèi)存泄漏 是 正在被別人使用的的內(nèi)存,不屬于垃圾內(nèi)存

堆引用內(nèi)存泄漏(Heap leak)

 1.靜態(tài)變量持有 已經(jīng)沒(méi)有用的對(duì)象,導(dǎo)致對(duì)象無(wú)法被回收.例如靜態(tài)集合類(lèi)引起內(nèi)存泄露

 2.單例中持有的引用,當(dāng)activity重新構(gòu)建后,單例持有的是上一個(gè)activity實(shí)例.導(dǎo)致上一個(gè)無(wú)法被回收.

 3.留意事件監(jiān)聽(tīng)器和回調(diào).如果一個(gè)類(lèi)注冊(cè)了監(jiān)聽(tīng)器,但當(dāng)該類(lèi)不再被使用后沒(méi)有注銷(xiāo)監(jiān)聽(tīng)器,可能會(huì)發(fā)生內(nèi)存泄漏。

 4.靜態(tài)內(nèi)部類(lèi),持有 對(duì)象.

 5.Handler 內(nèi)存泄漏

系統(tǒng)資源泄露(Resource Leak)

主要指程序使用系統(tǒng)分配的資源比如 Bitmap,handle ,SOCKET等沒(méi)有使用相應(yīng)的函數(shù)釋放掉,導(dǎo)致系統(tǒng)資源的浪費(fèi),嚴(yán)重可導(dǎo)致系統(tǒng)效能降低,系統(tǒng)運(yùn)行不穩(wěn)定。在try代碼塊里創(chuàng)建連接,在finally里釋放連接,就能夠避免此類(lèi)內(nèi)存泄漏。

   1.bitmap資源未釋放

   2.IO流未關(guān)閉

   3.Cursor使用完后未釋放

   4.各種連接(網(wǎng)絡(luò),數(shù)據(jù)庫(kù),socket等) 

4.內(nèi)存泄露的分析工具

在android studio 中有以下幾種工具,來(lái)進(jìn)行內(nèi)存泄漏優(yōu)化分析(eclipse也有類(lèi)似工具).

1.Memory Monitor 內(nèi)存監(jiān)視器.

2.Dump java heap

Android Device Monitor(eclipse系列工具類(lèi))

第三方庫(kù)LeakCanary

leakcanary的github地址


5.內(nèi)存泄露的實(shí)例解決方案

<font color="FF4500">與其說(shuō)解決內(nèi)存泄漏,更應(yīng)該說(shuō)是 避免內(nèi)存泄露 .因?yàn)閮?nèi)存泄漏一旦產(chǎn)生,即使需要重啟JVM,也就是重啟應(yīng)用,內(nèi)存重新開(kāi)始計(jì)算.即使這樣,也沒(méi)法解決</font>

1.單例造成的內(nèi)存泄露


/**
 * Created by ccj on 2016/11/3.
 */

public class SingleExample {

    private static SingleExample mExample;
    private Context context;

    private SingleExample(Context context) {
        this.context = context;
    }

    /**
     * 當(dāng)MainActivity銷(xiāo)毀再重建后,此時(shí)的context,不會(huì)走 if (mExample == null) ,而是直接返回.
     * 此時(shí)的Context 還是上一個(gè)activity實(shí)例的Context,所以,上一個(gè)activity實(shí)例并未被釋放,造成內(nèi)存泄漏
     * 
     * 此時(shí),只需要將application的上下文,作為context即可解決問(wèn)題
     * @param context
     * @return
     */
    public static SingleExample getExampleInstance(Context context) {
        if (mExample == null) {
            mExample = new SingleExample(context);
        }
        return mExample;

    }

}

2.匿名內(nèi)部類(lèi) 造成的內(nèi)存泄漏

//android開(kāi)發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View,此時(shí)如果你使用了匿名類(lèi),并被異步線程
//持有了,那要小心了,如果沒(méi)有任何措施這樣一定會(huì)導(dǎo)致泄露 
    public class MainActivity extends Activity {
        …
        Runnable ref1 = new MyRunable();
        Runnable ref2 = new Runnable() {
            @Override
            public void run() {
            }
        };
        …
    }

3.Handler 造成的內(nèi)存泄漏

Java對(duì)引用的分類(lèi)有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。

在Android應(yīng)用的開(kāi)發(fā)中,為了防止內(nèi)存溢出,在處理一些占用內(nèi)存大而且聲明周期較長(zhǎng)的對(duì)象時(shí)候,可以盡量應(yīng)用軟引用和弱引用技術(shù)。

軟/弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對(duì)象被垃圾回收器回收,Java虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián)的引用隊(duì)列中。利用這個(gè)隊(duì)列可以得知被回收的軟/弱引用的對(duì)象列表,從而為緩沖器清除已失效的軟/弱引用。

/*:在 Activity 中避免使用非靜態(tài)內(nèi)部類(lèi),比如上面我們將 Handler 聲明為靜態(tài)的,
    則其存活期跟 Activity 的生命周期就無(wú)關(guān)了。同時(shí)通過(guò)弱引用的方式引入 Activity,
    避免直接將 Activity 作為 context 傳進(jìn)去,

    推薦使用靜態(tài)內(nèi)部類(lèi) + WeakReference 這種方式。每次使用前注意判空

*/
public class SampleActivity extends Activity {
    private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Post a message and delay its execution for 10 minutes.
    mLeakyHandler.postDelayed(new Runnable() {
      @Override
      public void run() { /* ... */ }
    }, 1000 * 60 * 10);
    // Go back to the previous Activity.
    finish();
    }
}
//改進(jìn)機(jī)制

/*當(dāng)然在Activity銷(xiāo)毀時(shí)候也應(yīng)該取消相應(yīng)的任務(wù)AsyncTask.cancel(),避免任務(wù)在后臺(tái)執(zhí)行浪費(fèi)資源*/。
    public class MainActivity extends AppCompatActivity {
        private MyHandler mHandler = new MyHandler(this);
        private TextView mTextView ;
        private static class MyHandler extends Handler {
            private WeakReference<Context> reference;
            public MyHandler(Context context) {
                reference = new WeakReference<>(context);
            }
            @Override
            public void handleMessage(Message msg) {
                MainActivity activity = (MainActivity) reference.get();
                if(activity != null){
                    activity.mTextView.setText("");
                }
            }
        }

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTextView = (TextView)findViewById(R.id.textview);
            loadData();
        }

        private void loadData() {
//...request
            Message message = Message.obtain();
            mHandler.sendMessage(message);
        }
        //注意釋放
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mHandler.removeCallbacksAndMessages(null);
        }
    }

4.非靜態(tài)內(nèi)部類(lèi)創(chuàng)建靜態(tài)實(shí)例造成的內(nèi)存泄漏

/*這樣就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類(lèi)的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建,不過(guò)這種寫(xiě)法卻會(huì)造成內(nèi)存泄漏,因?yàn)榉庆o態(tài)內(nèi)部類(lèi)默認(rèn)會(huì)持有外部類(lèi)的引用,而又使用了該非靜態(tài)內(nèi)部類(lèi)創(chuàng)建了一個(gè)靜態(tài)的實(shí)例,該實(shí)例的生命周期和應(yīng)用的一樣長(zhǎng),這就導(dǎo)致了該靜態(tài)實(shí)例一直會(huì)持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正常回收。正確的做法為:
    將該內(nèi)部類(lèi)設(shè)為靜態(tài)內(nèi)部類(lèi)或?qū)⒃搩?nèi)部類(lèi)抽取出來(lái)封裝成一個(gè)單例,如果需要使用Context,請(qǐng)使用ApplicationContext*/

    public class MainActivity extends AppCompatActivity {
        private static TestResource mResource = null;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if(mManager == null){
                mManager = new TestResource();
            }
//...
        }
        class TestResource {
//...
        }
    }

5.資源未關(guān)閉造成的內(nèi)存泄漏

對(duì)于使用了BraodcastReceiver,ContentObserver,F(xiàn)ile,Cursor,Stream,Bitmap等資源的使用,應(yīng)該在Activity銷(xiāo)毀時(shí)及時(shí)關(guān)閉或者注銷(xiāo),否則這些資源將不會(huì)被回收,造成內(nèi)存泄漏。


6.內(nèi)存泄漏總結(jié)

1、對(duì)于生命周期比Activity長(zhǎng)的對(duì)象如果需要應(yīng)該使用ApplicationContext

2、在涉及到Context時(shí)先考慮ApplicationContext,當(dāng)然它并不是萬(wàn)能的,對(duì)于有些地方則必須使用Activity的Context,對(duì)于Application,Service,Activity三者的Context的應(yīng)用場(chǎng)景如下:
這里寫(xiě)圖片描述


其中:NO1表示Application和Service可以啟動(dòng)一個(gè)Activity,不過(guò)需要?jiǎng)?chuàng)建一個(gè)新的task任務(wù)隊(duì)列。而對(duì)于Dialog而言,只有在Activity中才能創(chuàng)建

3、對(duì)于需要在靜態(tài)內(nèi)部類(lèi)中使用非靜態(tài)外部成員變量(如:Context、View ),可以在靜態(tài)內(nèi)部類(lèi)中使用弱引用來(lái)引用外部類(lèi)的變量來(lái)避免內(nèi)存泄漏

4、對(duì)于生命周期比Activity長(zhǎng)的內(nèi)部類(lèi)對(duì)象,并且內(nèi)部類(lèi)中使用了外部類(lèi)的成員變量,可以這樣做避免內(nèi)存泄漏:

將內(nèi)部類(lèi)改為靜態(tài)內(nèi)部類(lèi)
靜態(tài)內(nèi)部類(lèi)中使用弱引用來(lái)引用外部類(lèi)的成員變量

5、對(duì)于不再需要使用的對(duì)象,顯示的將其賦值為null,比如使用完Bitmap后先調(diào)用recycle(),再賦為null

6、保持對(duì)對(duì)象生命周期的敏感,特別注意單例、靜態(tài)對(duì)象、全局性集合等的生命周期


About Me

github地址
個(gè)人技術(shù)成長(zhǎng)博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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