Android面試被問到內存泄漏了咋整?

前言

內存泄漏即該被釋放的內存沒有被及時的釋放,一直被某個或某些實例所持有卻再也不使用致使GC不能回收。
文末準備了一份完整系統的進階提高的技術大綱和學習資料,但願對於有必定工做經驗可是技術還須要提高的朋友提供一個方向參考,以及免去沒必要要的網上處處搜資料時間精力。javascript

Java內存分配策略

Java程序運行時的內存分配策略有三種,分別是靜態分配,棧式分配,和堆式分配。對應的三種策略使用的內存空間是要分別是靜態存儲區(也稱方法區),棧區,和堆區。java

  • 靜態存儲區(方法區):主要存放靜態數據,全局static數據和常量。這塊內存在程序編譯時就已經分配好,而且在程序整個運行期間都存在。android

  • 棧區:當方法執行時,方法內部的局部變量都創建在棧內存中,並在方法結束後自動釋放分配的內存。由於棧內存分配是在處理器的指令集當中因此效率很高,可是分配的內存容量有限。git

  • 堆區:又稱動態內存分配,一般就是指在程序運行時直接new出來的內存。這部份內存在不適用時將會由Java垃圾回收器來負責回收。github

棧與堆的區別:

在方法體內定義的(局部變量)一些基本類型的變量和對象的引用變量都在方法的棧內存中分配。當在一段方法塊中定義一個變量時,Java就會在棧中爲其分配內存,當超出變量做用域時,該變量也就無效了,此時佔用的內存就會釋放,而後會被從新利用。算法

堆內存用來存放全部new出來的對象(包括該對象內的全部成員變量)和數組。在堆中分配的內存,由Java垃圾回收管理器來自動管理。在堆中建立一個對象或者數組,能夠在棧中定義一個特殊的變量,這個變量的取值等於數組或對象在堆內存中的首地址,這個特殊的變量就是咱們上面提到的引用變量。咱們能夠經過引用變量來訪問堆內存中的對象或者數組。數組

舉個例子:架構

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 0;
        Sample mSample2 = new Sample();
    }
}
    Sample mSample3 = new Sample();
複製代碼

如上局部變量s2mSample2存放在棧內存中,mSample3所指向的對象存放在堆內存中,包括該對象的成員變量s1mSample1也存放在堆中,而它本身則存放在棧中。app

結論:異步

局部變量的基本類型和引用存儲在棧內存中,引用的實體存儲在堆中。——因它們存在於方法中,隨方法的生命週期而結束。

成員變量所有存儲於堆中(包括基本數據類型,引用和引用的對象實體)。——由於它們屬於類,類對象終究要被new出來使用。

瞭解了Java的內存分配以後,咱們再來看看Java是怎麼管理內存。

Java是如何管理內存

由程序分配內存,GC來釋放內存。內存釋放的原理爲該對象或者數組再也不被引用,則JVM會在適當的時候回收內存。

內存管理算法:

  1. 引用計數法:對象內部定義引用變量,當該對象被某個引用變量引用時則計數加1,當對象的某個引用變量超出生命週期或者引用了新的變量時,計數減1。任何引用計數爲0的對象實例均可以被GC。這種算法的優勢是:引用計數收集器能夠很快的執行,交織在程序運行中。對程序須要不被長時間打斷的實時環境比較有利。缺點:沒法檢測出循環引用。

引用計數沒法解決的循環引用問題以下:

public void method() {
        //Sample count=1
        Sample ob1 = new Sample();
        //Sample count=2
        Sample ob2 = new Sample();
        //Sample count=3
        ob1.mSample = ob2;
        //Sample count=4
        ob2.mSample = ob1;
        //Sample count=3
        ob1=null;
        //Sample count=2
        ob2=null;
        //計數爲2,不能被GC
    }
複製代碼

Java能夠做爲GC ROOT的對象有:虛擬機棧中引用的對象(本地變量表),方法區中靜態屬性引用的對象,方法區中常量引用的對象,本地方法棧中引用的對象(Native對象)

  1. 標記清除法:從根節點集合進行掃描,標記存活的對象,而後再掃描整個空間,對未標記的對象進行回收。在存活對象較多的狀況下,效率很高,可是會形成內存碎片。

  2. 標記整理算法:同標記清除法,只不過在回收對象時,對存活的對象進行移動。雖然解決了內存碎片的問題可是增長了內存的開銷。

  3. 複製算法:此方法爲克服句柄的開銷和解決堆碎片。把堆分爲一個對象面和多個空閒面。把存活的對象copy到空閒面,主要空閒面就變成了對象面,原來的對象面就變成了空閒面。這樣增長了內存的開銷,且在交換過程當中程序會暫停執行。

  4. 分代算法:

分代垃圾回收策略,是基於:不一樣的對象的生命週期是不同的。所以,不一樣生命週期的對象能夠採起不一樣的回收算法,以便提升回收效率。

年輕代:

  1. 全部新生成的對象首先都是存放在年輕代。年輕代的目標就是儘量快速的收集掉那些生命週期短的對象。

  2. 新生代內存按照8:1:1的比例分爲一個eden區和兩個survivor(survivor0,survivor1)區。一個Eden區,兩個 Survivor區(通常而言)。大部分對象在Eden區中生成。回收時先將eden區存活對象複製到一個survivor0區,而後清空eden區,當這個survivor0區也存放滿了時,則將eden區和survivor0區存活對象複製到另外一個survivor1區,而後清空eden和這個survivor0區,此時survivor0區是空的,而後將survivor0區和survivor1區交換,即保持survivor1區爲空, 如此往復。

  3. 當survivor1區不足以存放 eden和survivor0的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了就會觸發一次Full GC,也就是新生代、老年代都進行回收

  4. 新生代發生的GC也叫作Minor GC,MinorGC發生頻率比較高(不必定等Eden區滿了才觸發)

年老代:

  1. 在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。

  2. 內存比新生代也大不少(大概比例是1:2),當老年代內存滿時觸發Major GC即Full GC,Full GC發生頻率比較低,老年代對象存活時間比較長,存活率標記高。

持久代:

用於存放靜態文件,如Java類、方法等。持久代對垃圾回收沒有顯著影響,可是有些應用可能動態生成或者調用一些class,例如Hibernate 等,在這種時候須要設置一個比較大的持久代空間來存放這些運行過程當中新增的類。

Android常見的內存泄漏彙總

集合類泄漏

先看一段代碼

List<Object> objectList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
複製代碼

上面的實例,雖然在循環中把引用o釋放了,可是它被添加到了objectList中,因此objectList也持有對象的引用,此時該對象是沒法被GC的。所以對象若是添加到集合中,還必須從中刪除,最簡單的方法

//釋放objectList
        objectList.clear();
        objectList=null;
複製代碼

單例形成的內存泄漏

因爲單例的靜態特性使得其生命週期跟應用的生命週期同樣長,因此若是使用不恰當的話,很容易形成內存泄漏。好比下面一個典型的例子。

public class SingleInstanceClass {

    private static SingleInstanceClass instance;

    private Context mContext;

    private SingleInstanceClass(Context context) {
        this.mContext = context;
    }

    public SingleInstanceClass getInstance(Context context) {
        if (instance == null) {
            instance = new SingleInstanceClass(context);
        }
        return instance;
    }
}
複製代碼

正如前面所說,靜態變量的生命週期等同於應用的生命週期,此處傳入的Context參數即是禍端。若是傳遞進去的是Activity或者Fragment,因爲單例一直持有它們的引用,即使Activity或者Fragment銷燬了,也不會回收其內存。特別是一些龐大的Activity很是容易致使OOM。

正確的寫法應該是傳遞Application的Context,由於Application的生命週期就是整個應用的生命週期,因此沒有任何的問題。

public class SingleInstanceClass {

    private static SingleInstanceClass instance;

    private Context mContext;

    private SingleInstanceClass(Context context) {
        this.mContext = context.getApplicationContext();// 使用Application 的context
    }

    public SingleInstanceClass getInstance(Context context) {
        if (instance == null) {
            instance = new SingleInstanceClass(context);
        }
        return instance;
    }
}

or

//在Application中定義獲取全局的context的方法
 /** * 獲取全局的context * @return 返回全局context對象 */
    public static Context getContext(){
        return context;
    }

public class SingleInstanceClass {

    private static SingleInstanceClass instance;

    private Context mContext;

    private SingleInstanceClass() {
       mContext=MyApplication.getContext;
    }

    public SingleInstanceClass getInstance() {
        if (instance == null) {
            instance = new SingleInstanceClass();
        }
        return instance;
    }
}

複製代碼

匿名內部類/非靜態內部類和異步線程

  • 非靜態內部類建立靜態實例形成的內存泄漏
    咱們都知道非靜態內部類是默認持有外部類的引用的,若是在內部類中定義單例實例,會致使外部類沒法釋放。以下面代碼:
public class TestActivity extends AppCompatActivity {
    public static InnerClass innerClass = null;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (innerClass == null)
            innerClass = new InnerClass();
    }
    private class InnerClass {
        //...
    }
}
複製代碼

TestActivity銷燬時,由於innerClass生命週期等同於應用生命週期,可是它又持有TestActivity的引用,所以致使內存泄漏。

正確作法應將該內部類設爲靜態內部類或將該內部類抽取出來封裝成一個單例,若是須要使用Context,請按照上面推薦的使用Application 的 Context。固然,Application 的 context 不是萬能的,因此也不能隨便亂用,對於有些地方則必須使用 Activity 的 Context,對於Application,Service,Activity三者的Context的應用場景以下:


  • 匿名內部類
    android開發常常會繼承實現Activity/Fragment/View,此時若是你使用了匿名類,並被異步線程持有了,那要當心了,若是沒有任何措施這樣必定會致使泄露。以下代碼:
public class TestActivity extends AppCompatActivity {
  //....

    private Runnable runnable=new Runnable() {
        @Override
        public void run() {

        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       //......
    }

}
複製代碼

上面的runnable所引用的匿名內部類持有TestActivity的引用,當將其傳入異步線程中,線程與Activity生命週期不一致就會致使內存泄漏。

  • Handler形成的內存泄漏
    Handler形成內存泄漏的根本緣由是由於,Handler的生命週期與Activity或者View的生命週期不一致。Handler屬於TLS(Thread Local Storage)生命週期同應用週期同樣。看下面的代碼:
public class TestActivity extends AppCompatActivity {
    private Handler mHandler = new Handler() {
        @Override
        public void dispatchMessage(Message msg) {
            super.dispatchMessage(msg);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
            //do your things
            }
        }, 60 * 1000 * 10);

        finish();
    }
}
複製代碼

在該TestActivity中聲明瞭一個延遲10分鐘執行的消息 MessagemHandler將其 push 進了消息隊列 MessageQueue 裏。當該 Activity 被finish()掉時,延遲執行任務的Message還會繼續存在於主線程中,它持有該 Activity 的Handler引用,因此此時 finish()掉的 Activity 就不會被回收了從而形成內存泄漏(因 Handler 爲非靜態內部類,它會持有外部類的引用,在這裏就是指TestActivity)。

修復方法:採用內部靜態類以及弱引用方案。代碼以下:

public class TestActivity extends AppCompatActivity {
    private MyHandler mHandler;

    private static class MyHandler extends Handler {
        private final WeakReference<TestActivity> mActivity;

        public MyHandler(TestActivity activity) {
            mActivity = new WeakReference<>(activity);
        }

        @Override
        public void dispatchMessage(Message msg) {
            super.dispatchMessage(msg);
            TestActivity activity = mActivity.get();
            //do your things
        }
    }

    private static final Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            //do your things
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler = new MyHandler(this);
        mHandler.postAtTime(mRunnable, 1000 * 60 * 10);

        finish();
    }

}

複製代碼

須要注意的是:使用靜態內部類 + WeakReference 這種方式,每次使用前注意判空。

前面提到了 WeakReference,因此這裏就簡單的說一下 Java 對象的幾種引用類型。

Java對引用的分類有 Strong reference, SoftReference, WeakReference, PhatomReference 四種。



ok,繼續回到主題。前面所說的,建立一個靜態Handler內部類,而後對 Handler 持有的對象使用弱引用,這樣在回收時也能夠回收 Handler 持有的對象,可是這樣作雖然避免了Activity泄漏,不過Looper 線程的消息隊列中仍是可能會有待處理的消息,因此咱們在Activity的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息。

下面幾個方法均可以移除 Message:

public final void removeCallbacks(Runnable r);

public final void removeCallbacks(Runnable r, Object token);

public final void removeCallbacksAndMessages(Object token);

public final void removeMessages(int what);

public final void removeMessages(int what, Object object);
複製代碼

儘可能避免使用 staic 成員變量

若是成員變量被聲明爲 static,那咱們都知道其生命週期將與整個app進程生命週期同樣。

這會致使一系列問題,若是你的app進程設計上是長駐內存的,那即便app切到後臺,這部份內存也不會被釋放。按照如今手機app內存管理機制,佔內存較大的後臺進程將優先回收,意味着若是此app作過進程互保保活,那會形成app在後臺頻繁重啓。就會出現一晚上時間手機被消耗空了電量、流量,這樣只會被用戶棄用。
這裏修復的方法是:

不要在類初始時初始化靜態成員。能夠考慮lazy初始化。
架構設計上要思考是否真的有必要這樣作,儘可能避免。若是架構須要這麼設計,那麼此對象的生命週期你有責任管理起來。

  • 避免 override finalize():
  1. finalize 方法被執行的時間不肯定,不能依賴與它來釋放緊缺的資源。時間不肯定的緣由是: 虛擬機調用GC的時間不肯定以及Finalize daemon線程被調度到的時間不肯定。

  2. finalize 方法只會被執行一次,即便對象被複活,若是已經執行過了 finalize 方法,再次被 GC 時也不會再執行了,緣由是:含有 finalize 方法的 object 是在 new 的時候由虛擬機生成了一個 finalize reference 在來引用到該Object的,而在 finalize 方法執行的時候,該 object 所對應的 finalize Reference 會被釋放掉,即便在這個時候把該 object 復活(即用強引用引用住該 object ),再第二次被 GC 的時候因爲沒有了 finalize reference 與之對應,因此 finalize 方法不會再執行。

  3. 含有Finalize方法的object須要至少通過兩輪GC纔有可能被釋放。

其它

內存泄漏檢測工具強烈推薦 squareup 的 LeakCannary,但須要注意Android版本是4.4+的,不然會Crash。


+qq羣:853967238。獲取以上高清技術思惟圖,以及相關技術的免費視頻學習資料

相關文章
相關標籤/搜索