結合Android Studio和MAT檢測並簡單分析內存泄露

1.什麼是GC?

在分析內存泄露之前首先要了解一下GC,GC(Garbage Collection)就是Java中常提到的垃圾回收,指的是JVM會自動回收不在被引用的內存數據。

2.什麼是GC Roots?

GC Roots即Java虛擬機當前存活的對象集,其中的每一個對象可以作爲一個GC Root。
在確定一個對象是否需要被回收時,常常會用到可達性分析算法,即通過判斷對象的引用鏈是否可達來決定對象是否可以被回收,如下圖:
GC Root
上圖中,從GC Root到ObjectC、ObjectE、ObjectF都不可達,所以這三個對象就是會被GC回收的對象。

3.什麼是內存泄露?

內存泄露就是指某些已經沒有被使用的對象還存在內存之中,並且GC無法回收它們。如果任由它們常駐內存,程序的運行性能就會受到影響。
造成內存泄露的根本原因就是因爲對象在沒有使用的時候仍存在一條到GC Root的可達路徑,導致GC無法正常回收他們,如下圖:
內存泄露
已經使用不到的ObjectC任然間接與GC Root相連,這就會導致GC無法回收它,從而導致內存泄露。

4.如何檢測並定位內存泄露?

這裏主要介紹兩種定位內存泄露的方法:

4.1 使用Androd Studio的Android Monitor裏面的Memory

如下圖:
Android Monintor
先來看看正常情況下是怎麼顯示的:
在MainActivity裏面添加一個按鈕,並給他綁定如下的點擊事件:

public class MainActivity extends AppCompatActivity {
    Button mButton;
    List<TextView> List = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewById(R.id.btn_add);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i = 0 ; i<= 20000;i++){
                    TextView textView = new TextView(MainActivity.this);
                    List.add(textView);
                }

            }

        });

    }
}

如果這時候點擊了Button,內存會這樣變化:
GC前
如果這時候按下返回鍵,退出MainActivity,然後手動調用GC(點一下小卡車標誌),內存會這樣變化:
GC後
可以明顯看到GC後的內存又降了下來,說明無用的TextView和ArrayList對象被GC正常回收了。
下面構建一個會出現內存泄露場景:一個靜態的Context對象持有了當前Activity的引用。(當然在實際開發中多半不會遇到)

public class MainActivity extends AppCompatActivity {
    static Context mContext;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewById(R.id.btn_add);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i = 0 ; i<= 20000;i++){
                    TextView textView = new TextView(MainActivity.this);
                    List.add(textView);
                }

            }

        });
        mContext = this;
    }
}

執行和剛剛相同的操作,先點Button,再點返回鍵退出Activity,然後手動GC,發現內存會居高不下:
內存泄露
這時候點擊如圖所示的Dump Java Heap按鈕,等待Dump完成,自動調轉到分析界面:
分析hprof文件
先點擊綠色小三角,然後點開Leak Activitys,即可看存在內存泄露的Activity和內存泄露的位置。
根據圖中的信息可以清楚地看到是mContext出現了內存泄露,然後再回到代碼中找到mContext,即可着手解決這個問題。

4.2使用MAT

MAT(Memory Analyzer)是一款強大的Java堆內存分析工具,可以快速分析內存情況,是由eclipse公司推出的。 它並不會直接告訴我們內存泄露的具體位置,只會給出一些內存和引用信息,我們需要根據這些信息來排查可能出現內存泄露的地方,適合比較複雜的內存泄露的情景。

4.2.1獲取hprof文件

它也需要藉助hprof文件來分析內存,所以首先得要獲取hprof文件,獲取它的方式有兩種,一種就是剛剛使用的Android Monitor中的Dump Java Heap,但是獲取到的hprof文件需要稍作轉換才能供MAT使用,如圖:
轉換hprof
還有一種方式就是通過DDMS來獲取hprof,如圖:
打開ADM
獲取hprof
打開DDMS先選擇heap標籤,然後在左邊的方框裏選擇當前的應用的包名,再點一下上方updata heap(綠色小圓柱),之後點擊操作一下App(點一下button,然後按下返回鍵),然後點擊右邊的Cause GC按鈕,最後點擊Dump HPROF file(帶紅色箭頭的綠色小圓柱),即可導出hprof文件。
同樣,這種方式獲取到的hprof文件同樣需要轉換一下,這裏要採用hprof-conv命令轉換,如圖:

轉換hprof
格式爲:hprof-conv + 輸入文件路徑 + 輸出文件路徑及名稱

4.2.2使用MAT排查內存泄露位置

MAT主界面
在排查之前,先了解幾個名詞:
Dominator Tree:支配樹,列出每個對象、大小,常用於分析對象之間的引用結構。(站在實例的角度)
Histogram:直方圖,列出內存中每個對象的名字,大小和數量。(站在類的角度)
Shallow heap:對象本身佔用內存的大小。
Retained Heap:這個對象以及它所持有的其它引用(包括直接和間接)所佔的總內存。
outgoing references:被當前對象引用的對象。通過它可以看出當前當前”抓着”哪些對象不放。
incoming references:引用到該對象的對象。通過它可以看出誰在內存中”抓着”當前實例不放。

再來看一個常見的內存泄露的場景:

public class MainActivity extends AppCompatActivity {
    static LeakClazz mLeakClazz;
    List<TextView> List = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         //這個for循環只是爲了加大內存佔用,讓內存顯示得更明顯
        for (int i = 0 ; i<= 10000;i++){
            TextView textView = new TextView(MainActivity.this);
            List.add(textView);
        }

        if (mLeakClazz == null){
            mLeakClazz = new LeakClazz();
        }
    }

    class LeakClazz{

    }
}

衆所周知,一個非靜態的內部類會持有外部類的引用,然而在這裏卻創建了一個靜態的實例mLeakClazz,那隻要app進程還存在,mLeakClazz就存在,所以外部類MainActivity就會被一直引用,及時他已經不在使用了。這就造成了內存泄露。在獲取到hprof文件後,我們來分析一下內存佔用情況。
首先打開獲取到的leak.hprof文件:
打開hprof
然後點開Dominator Tree查看對象之間引用結構:
打開Dominator Tree
我們可以使用正則搜索功能,直接搜索與MainActivity有關的引用信息:
搜索MainActivity
找出與GC Roots相連的路徑,由於軟引用、弱引用和虛引用並不會影響GC的運行,所以可以直接過濾掉他們:
過濾掉無用的引用
這樣就可以找到內存泄露的位置了:
找到內存泄露的位置
至此,一個就可以去代碼中修改內存泄露的地方。整個過程看似簡單,這裏這時簡單的演示了一下,但在實際開發中需要靠經驗和工具的結合方可定位內存泄露的地方,下面就總結一下常用內存泄露的場景:
1.創建了非靜態的內部類的靜態實例
上面的例子已經分析過了。
2.單例模式持有外部對象的引用
單例對象會在JVM中以靜態變量的形式一直存在,如果持有了外部對象的引用,那麼外部對象將無法被GC回收,特別是在單例模式中需要用到context的時候,如果能夠使用Application的context就儘量使用,如果不能,就要考慮能不能使用軟引用或弱引用了。下面是一個單例模式可能造成內存泄露的場景:

public class Singleton {
    public Context mContext;
    private Singleton(){}
    private static class Holder{
        private static Singleton INSTANCE = new Singleton();
    }
    public Singleton getINSTANCE(){
        return Holder.INSTANCE;
    }

    public void doSomething(Context context){
        mContext = context;
    }
}

如果調用doSomething(Context context)方法的時候傳入了Activity的Context就必然會造成內存泄露。
3.使用線程進行耗時操作的時候沒有與Activity的生命週期保持一致
線程使用的是內部類或者Runnable實現的時候由於持有外部類(Activity)的引用,若是在Activity退出的時候線程還沒執行完,它將一直持有Activity的引用,這就會造成內存泄露。就像這樣:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

          //這個for循環只是爲了加大內存佔用,讓內存顯示得更明顯
        for (int i = 0 ; i<= 10000;i++){
            TextView textView = new TextView(MainActivity.this);
            List.add(textView);
        }

         //匿名內部類會持有外部類的引用
        new Thread(new Runnable() {
            @Override
            public void run() {
                //模擬一個耗時操作
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

解決的方法就是在activity被finish()的時候結束線程:

public class MainActivity extends AppCompatActivity {
    Thread t;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        for (int i = 0 ; i<= 10000;i++){
            TextView textView = new TextView(MainActivity.this);
            List.add(textView);
        }

        t = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("test", "run: ");
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        t.interrupt();
    }
}

4.使用一些系統資源後未及時關閉
使用了BraodcastReceiver,File, Cursor,Stream,Bitmap等資源後一定要及時關閉,不然也會造成內存泄露。

總而言之,熟悉一些常見的內存泄露場景,再瞭解一下該如何藉助工具去分析內存泄露,就可以在實際開發中大大避免這一問題。
此外, leakcanary在一定程度上還更加容易幫助我們檢測內存泄露,也是一款神器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章