完全搞懂Java內存泄露

以前一直在簡書寫做,第一次發佈到SF上來,也是第一次使用SF,後面會盡可能同步到SF,更多文章請關注:
簡書 編程之樂
轉載請註明出處:謝謝!java

Java內存回收方式

Java判斷對象是否能夠回收使用的而是可達性分析算法。android

在主流的商用程序語言中(Java和C#),都是使用可達性分析算法判斷對象是否存活的。這個算法的基本思路就是經過一系列名爲"GC Roots"的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的,下圖對象object5, object6, object7雖然有互相判斷,但它們到GC Roots是不可達的,因此它們將會斷定爲是可回收對象。git

Paste_Image.png

在Java語言裏,可做爲GC Roots對象的包括以下幾種:github

a.虛擬機棧(棧楨中的本地變量表)中的引用的對象
    b.方法區中的類靜態屬性引用的對象
    c.方法區中的常量引用的對象
    d.本地方法棧中JNI的引用的對象

摘自《深刻理解Java虛擬機》算法

使用leakcanary檢測泄漏

關於LeakCanary使用參考如下文章:編程

  1. LeakCanary: 讓內存泄露無所遁形      
  2. LeakCanary 中文使用說明

LeakCanary的內存泄露提示通常會包含三個部分:
第一部分(LeakSingle類的sInstance變量)
引用第二部分(LeakSingle類的mContext變量),
致使第三部分(MainActivity類的實例instance)泄露.閉包

Paste_Image.png

leakcanary使用注意

即便是空的Activity,若是檢測泄露時候遇到了以下這樣的泄露,注意,把refWatcher.watct()放在onDestroy裏面便可解決,或者忽略這樣的提示。
因爲文章已寫不少,下面的就再也不修改,忽略這種錯誤便可。app

* com.less.demo.TestActivity has leaked:
* GC ROOT static android.app.ActivityThread.sCurrentActivityThread
* references android.app.ActivityThread.mActivities
* references android.util.ArrayMap.mArray
* references array java.lang.Object[].[1]
* references android.app.ActivityThread$ActivityClientRecord.activity
* leaks com.less.demo.TestActivity instance
protected void onDestroy() {
    super.onDestroy();
    RefWatcher refWatcher = App.getRefWatcher(this);
    refWatcher.watch(this);
}

leakcanary和代碼示例說明內存泄露

案例一(靜態成員引發的內存泄露)less

public class App extends Application {
    private RefWatcher refWatcher;

    @Override
    public void onCreate() {
        super.onCreate();
        refWatcher = LeakCanary.install(this);
    }

    public static RefWatcher getRefWatcher(Context context) {
        App application = (App) context.getApplicationContext();
        return application.refWatcher;
    }
}

測試內部類持有外部類引用,內部類是靜態的(GC-ROOT,將一直連着這個外部類實例),靜態的會和Application一個生命週期,這會致使一直持有外部類引用(內部類隱含了一個成員變量$0), 即便外部類制空= null,也沒法釋放。ide

OutterClass

public class OutterClass {
    private String name;

    class Inner{
        public void list(){
            System.out.println("outter name is " + name);
        }
    }
}

TestActivity

public class TestActivity extends Activity {
    // 靜態的內部類實例
    private static OutterClass.Inner innerClass;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        OutterClass outterClass = new OutterClass();
        innerClass = outterClass.new Inner();

        RefWatcher refWatcher = App.getRefWatcher(this);
        refWatcher.watch(outterClass);// 監控的對象

        outterClass = null;
    }

Paste_Image.png

案例二(單例模式引發的內存泄露)

DownloadManager

public class DownloadManager {
    private static DownloadManager instance;
    private Task task ;

    public static DownloadManager getInstance(){
        if (instance == null) {
            instance = new DownloadManager();
        }
        return instance;
    }
    public Task newTask(){
        this.task = new Task();
        return task;
    }
}

Task

public class Task {
    private Call call;
    public Call newCall(){
        this.call = new Call();
        return call;
    }
}

Call

public class Call {
    public void execute(){
        System.out.println("=========> execute call");
    }
}

TestActivity

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        RefWatcher refWatcher = App.getRefWatcher(this);

        Task task = DownloadManager.getInstance().newTask();
        Call call = task.newCall();
        call.execute();
        refWatcher.watch(call);// 監控的對象
        call = null; // 沒法回收,DownloadManager是靜態單例,引用task,task引用了call,即便call置爲空,也沒法回收,切斷GC_ROOT 聯繫便可避免內存泄露,即置task爲空。
    }
}

Paste_Image.png

部分日誌打印以下:當前的GC_ROOT是DownloadManager的instance實例。

In com.leakcanary.demo:1.0:1.
* com.less.demo.Call has leaked:
* GC ROOT static com.less.demo.DownloadManager.instance
* references com.less.demo.DownloadManager.task
* references com.less.demo.Task.call
* leaks com.less.demo.Call instance

關於上面兩種方式致使的內存泄露問題,這裏再舉兩個案例說明以增強理解。

案例三(靜態變量致使的內存泄露)

public class TestActivity extends Activity {
    private static Context sContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        sContext = this;

        RefWatcher refWatcher = App.getRefWatcher(this);

        refWatcher.watch(this);// 監控的對象
    }

Paste_Image.png

打印日誌以下:

In com.leakcanary.demo:1.0:1.
com.less.demo.TestActivity has leaked:
GC ROOT static com.less.demo.TestActivity.sContext
leaks com.less.demo.TestActivity instance

從這段日誌能夠分析出:聲明static後,sContext的生命週期將和Application同樣長,Activity即便退出到桌面,Application依然存在->sContext依然存在,GC此時想回收Activity卻發現Activity仍然被sContext(GC-ROOT鏈接着),致使死活回收不了,內存泄露。

上面的代碼改造一下,以下。

public class TestActivity extends Activity {
    private static View sView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        sView = new View(this);

        RefWatcher refWatcher = App.getRefWatcher(this);

        refWatcher.watch(this);
    }
}

Paste_Image.png
日誌以下

In com.leakcanary.demo:1.0:1.
com.less.demo.TestActivity has leaked:
GC ROOT static com.less.demo.TestActivity.sView
references android.view.View.mContext
leaks com.less.demo.TestActivity instance

案例四(單例模式致使的內存泄露)
DownloadManager

public class DownloadManager {
    private static DownloadManager instance;
    private List<DownloadListener> mListeners = new ArrayList<>();

    public interface DownloadListener {
        void done();
    }

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

    public void register(DownloadListener downloadListener){
        if (!mListeners.contains(downloadListener)) {
            mListeners.add(downloadListener);
        }
    }

    public void unregister(DownloadListener downloadListener){
        if (mListeners.contains(downloadListener)) {
            mListeners.remove(downloadListener);
        }
    }
}

TestActivity

public class TestActivity extends Activity implements DownloadManager.DownloadListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        DownloadManager.getInstance().register(this);

        RefWatcher refWatcher = App.getRefWatcher(this);
        refWatcher.watch(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 忘記 unregister
        // DownloadManager.getInstance().unregister(this);
    }

    @Override
    public void done() {
        System.out.println("done!");
    }
}

Paste_Image.png

In com.leakcanary.demo:1.0:1.
* com.less.demo.TestActivity has leaked:
* GC ROOT static com.less.demo.DownloadManager.instance
* references com.less.demo.DownloadManager.mListeners
* references java.util.ArrayList.array
* references array java.lang.Object[].[0]
* leaks com.less.demo.TestActivity instance

錯誤寫法必定致使內存泄露嗎?

答案是:否認的。
以下案例,有限時間內是能夠挽救內存泄露發生的,因此控制好生命週期,其餘狀況:如單例對象(靜態變量)的生命週期比其持有的sContext
的生命週期更長時 ->內存泄露,更短時->躲過內存泄露。

public class TestActivity extends Activity {
    private static Context sContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        sContext = this;

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                sContext = null; 
            }
        },1000);// 分別測試1s和12s,前者不會內存泄露,後者必定泄露。因此若是趕在GC以前切斷GC_ROOT是能夠避免內存泄露的。
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = App.getRefWatcher(this);
        refWatcher.watch(this);
    }
}

Handler 引發的內存泄露詳細分析

handler致使的內存泄露可能咱們大多數都犯過.
注意如下代碼中的註釋,非靜態內部類雖然持有外部類引用,可是持有並不表明必定泄露,而是看gc時誰的命長。通過測試 狀況(1)始終沒有內存泄露。

爲何會這樣, 很早閱讀Handler源碼時候記得這幾個貨都是互相引用來引用去的,Message有個target字段, message.target = handler;
handler.post(message);又把這個message推入了MessageQueue中,而MessageQueue是在一個Looper線程中不斷輪詢處理消息,而有時候message仍是個老不死,可以重複利用。若是當Activity退出時候,還有消息未處理或正在處理,因爲message引用handler,handler又引用Activity,此時將引起內存泄露。

public class TestActivity extends Activity {
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            System.out.println("===== handle message ====");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        // (1) 不會致使內存泄露
        handler.sendEmptyMessageDelayed(0x123,0);

        // (2) 會致使內存泄露,非靜態內部類(包括匿名內部類,好比這個 Handler 匿名內部類)會引用外部類對象 this(好比 Activity)
        // 當它使用了 postDelayed 的時候,若是 Activity 已經 finish 了,而這個 handler 仍然引用着這個 Activity 就會導致內存泄漏
        // 由於這個 handler 會在一段時間內繼續被 main Looper 持有,致使引用仍然存在.
        handler.sendEmptyMessageDelayed(0x123, 12000);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = App.getRefWatcher(this);
        refWatcher.watch(this);
    }
}

Paste_Image.png

com.less.demo.TestActivity has leaked:
* GC ROOT android.view.inputmethod.InputMethodManager$ControlledInputConnectionWrapper.mH
* references com.android.internal.view.IInputConnectionWrapper$MyHandler.mQueue
* references android.os.MessageQueue.mMessages
* references android.os.Message.target , matching exclusion field android.os.Message#target
* references com.less.demo.TestActivity$1.this$0 (anonymous subclass of android.os.Handler)
* leaks com.less.demo.TestActivity instance

知道了原理後,即便寫出易於內存泄露的代碼也可以避免內存泄露啦。
上述代碼只需在onDestroy()函數中調用mHandler.removeCallbacksAndMessages(null);
在Activity退出的時候的移除消息隊列中全部消息和全部的Runnable。

內部類引發的內存泄露

內部類種類大體以下:

  1. 非靜態內部類(成員內部類)
  2. 靜態內部類(嵌套內部類)
  3. 局部內部類(定義在方法內或者做用域內的類,好似局部變量,因此不能有訪問控制符和static等修飾)
  4. 匿名內部類(沒有名字,僅使用一次new個對象即扔掉類的定義)

爲何非靜態內部類持有外部類引用,靜態內部類不持有外部引用。

這個問題很是簡單,就像 static的方法只能調用static的東西,非static能夠調用非static和static的同樣。static--> 針對class, 非static->針對 對象,我是這麼簡單理解的。看圖:

Paste_Image.png

匿名內部類
將局部內部類的使用再深刻一步,假如只建立某個局部內部類的一個對象,就沒必要命名了。

匿名內部類的類型能夠是以下幾種方式。

  1. 接口匿名內部類
  2. 抽象類匿名內部類
  3. 類匿名內部類

匿名內部類總結:

  1. 其實主要就是類定義一次就失效了,主要使用的是這個類(不知道名字)的實例。根據內部類的特性,可以實現回調和閉包。
  2. JavaScript和Python的回調傳遞的是fuction,Java傳遞的是object。

Java中經常用到回調,而回調的本質就是傳遞一個對象,JavaScript或其餘語言則是傳遞一個函數(如Python,或者C,使用函數指針的方式),因爲傳遞一個對象能夠攜帶其餘的一些信息,因此Java認爲傳遞一個對象比傳遞一個函數要靈活的多(固然java也能夠用Method反射對象傳遞函數)。參考《Java核心技術》

非靜態內部類致使內存泄露(前提dog的命長)
下面的案例將致使內存泄露
其中private static Dog dog ; 致使Dog的命比TestActivity長,這就糟糕了,可是注意,若是改成private Dog dog ; 即便Dog是非靜態內部類,也不會致使內存泄露,要死也是Dog先死,畢竟Dog是TestActivity的家庭成員,開掛也得看主人。

public class TestActivity extends Activity {
    private static Dog dog ;

    class Dog {
        public void say(){
            System.out.println("I am lovely dog!");
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        dog = new Dog();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = App.getRefWatcher(this);
        refWatcher.watch(this);
    }
}

Paste_Image.png

In com.leakcanary.demo:1.0:1.
* com.less.demo.TestActivity has leaked:
* GC ROOT static com.less.demo.TestActivity.dog
* references com.less.demo.TestActivity$Dog.this$0
* leaks com.less.demo.TestActivity instance

哪些內部類或者回調函數是否持有外部類對象?
一個反射案例說明一切

/**
 * 做者: limitless
 * 描述: 一個案例測試全部類型內部類對外部類對象的持有狀況
 * 網站: https://github.com/wangli0
 */
public class Main {

    /* 持有外部類引用 */
    private IAppListener mAppListener = new IAppListener() {
        private String name;
        @Override
        public void done() {
            System.out.println("匿名內部類對象做爲成員變量");
        }
    };
    /* 未持有 */
    private static IAppListener sAppListener = new IAppListener() {
        private String name;
        @Override
        public void done() {
            System.out.println("匿名內部類對象做爲static成員變量");
        }
    };

    public static void main(String args[]) {
        Main main = new Main();
        main.test1();
        main.test2();
        main.test3();// test3 《=》test4
        main.test4();
        main.test5();
        main.test6();
    }

    class Dog {
        private String name;
    }

    /* 持有外部類引用 */
    public void test1(){
        Dog dog = new Dog();
        getAllFieldName(dog.getClass());
    }

    static class Cat {
        private String name;
    }
    /* 未持有 */
    private void test2() {
        Cat cat = new Cat();
        getAllFieldName(cat.getClass());
    }

    /* 持有外部類引用 */
    private void test3() {
        class Monkey{
            String name;
        }
        Monkey monkey = new Monkey();
        getAllFieldName(monkey.getClass());
    }

    /* 持有外部類引用 */
    private void test4() {
        // 經常使用做事件回調的地方(有可能引發內存泄露)
        IAppListener iAppListener = new IAppListener() {
            private String name;
            @Override
            public void done() {
                System.out.println("匿名內部類");
            }
        };
        getAllFieldName(iAppListener.getClass());
    }

    /* 持有外部類引用 */
    private void test5() {
        getAllFieldName(mAppListener.getClass());
    }

    /* 未持有 */
    private void test6() {
        getAllFieldName(sAppListener.getClass());
    }

    private void getAllFieldName(Class<?> clazz) {
        System.out.println("className: ======> " + clazz.getSimpleName());
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field.getName());
        }
    }
}

Paste_Image.png

上述結果足夠說明,即便是方法內的回調對象也是持有外部類引用的,那麼雖然做用域是局部的,也有存在內存泄露的可能。我屢次強調 持有某對象 不表明必定泄露,看的是誰命長。回調在Android開發過程當中幾乎到處可見,若是持有就會內存泄露的話,那幾乎就無法玩了。
通常狀況下,咱們經常設置某個方法內的xx.execute(new Listener(){xx});是不會致使內存泄露的,這個方法執行完,局部做用域就失效了。可是若是在execute(listener);過程當中,某個單例模式的對象 忽然引用了這個listener對象,那麼就會致使內存泄露。

下面用實例證實個人想法
TestActivity

public class TestActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        Task task = new Task();
        task.execute(new ICallback() {
            @Override
            public void done() {
                System.out.println("下載完成!");
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = App.getRefWatcher(this);
        refWatcher.watch(this);
    }
}

Task

public class Task {
    public void execute(ICallback iCallback) {
        DownloadManager.getInstance().execute(iCallback);
    }
}

DownloadManager

public class DownloadManager {
    public static DownloadManager instance;
    private ICallback mICallback;
    public static DownloadManager getInstance(){
        if (instance == null) {
            instance = new DownloadManager();
        }
        return instance;
    }

    public void execute(ICallback iCallback) {
        this.mICallback = iCallback;// 反例,千萬不要這麼作,將致使內存泄露,若是註釋掉這行,內存泄露不會發生
        iCallback.done();
    }

Paste_Image.png

這足以證實個人想法是正確的,Callback的不巧當使用一樣會致使內存泄露 的發送。

總結

  1. 若是某些單例須要使用到Context對象,推薦使用Application的context,不要使用Activity的context,不然容易致使內存泄露。單例對象的生命週期和Application一致,這樣Application和單例對象就一塊兒銷燬。
  2. 優先使用靜態內部類而不是非靜態的,由於非靜態內部類持有外部類引用可能致使垃圾回收失敗。若是你的靜態內部類須要宿主Activity的引用來執行某些東西,你要將這個引用封裝在一個WeakReference中,避免意外致使Activity泄露,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收 只被弱引用關聯 的對象,只被 說明這個對象自己已經沒有用處了。
public class TestActivity extends Activity {
    private MyHandler myHandler = new MyHandler(this);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        
    }

    static class MyHandler extends Handler {
        private WeakReference<Activity> mWeakReference;

        public MyHandler(Activity activity){
            mWeakReference = new WeakReference<Activity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Toast.makeText(mWeakReference.get(), "xxxx", Toast.LENGTH_LONG).show();
            Log.d("xx", mWeakReference.get().getPackageName());
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = App.getRefWatcher(this);
        refWatcher.watch(this);
    }
}
相關文章
相關標籤/搜索