內存泄漏與排查流程——安卓性能優化

前言

內存泄漏能夠說是安卓開發中常遇到的問題,追溯和排查其問題根源是進階的程序猿必須具有的一項技能。小盆友今天便與你們分享一下這方面的一些看法,若有理解錯誤或是不一樣看法,能夠於評論區留言咱們進行討論,若是喜歡給個贊鼓勵下吧。php

篇幅較長,能夠經過目錄尋找本身所需瞭解的吧java

目錄

一、JAVA內存解析
二、JAVA回收機制
三、四種引用
四、小結
五、安卓內存泄漏排查工具
六、內存泄漏檢查與解決流程
七、常見的內存泄漏緣由
算法

一、JAVA內存解析

要想知道內存泄漏,須要先了解java中運行時內存是怎麼構成的,才能知道是哪一個地方致使。話很少說,先上圖 數組

java內存模型
運行時的java內存分爲兩大塊: 線程私有(藍色區域)、 共享數據區(黃色區域)
線程私有:主要用於存儲各個線程私有的一些信息,包括:程序計數器、虛擬機棧、本地方法棧
共享數據區:主要用於存儲公用的一些信息,包括:方法區(內含常量池)、堆

  1. 程序計數器:讓程序中各個線程知道本身接下來須要執行哪一行。在java中多線程爲搶佔式(由於cpu在某一時刻只會執行一條線程),當線程切換時,須要繼續哪一行便由程序計數器告知。

    舉個例子:A、B兩條線程,此時CPU執行從A切換至B,過了段時間從B切換回A,此時A須要從上次暫停的地方繼續執行,此時從哪一行執行就是由程序計數器來提供。

    值得一提
    (1)若執行java函數時,程序計數器記錄的是虛擬機字節碼的地址;
    (2)若執行native方法時,程序計數器便置爲了null。
    (3)在java虛擬機規範中,程序計數器是惟一沒有定義OutOfMemoryError。
    bash

  2. 虛擬機棧:描述的是java方法的內存模型,平時說的「棧」其實就是虛擬機棧,其生命週期與線程相同。每一個方法(不包含native方法)執行的同時都會建立一個棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。

    值得一提:在java虛擬機規範中,此處定義了兩個異常
    (1)StackOverFlowError (在遞歸中常看到,遞歸層級過深)
    (2)OutOfMemoryError
    微信

  3. 本地方法棧:是爲虛擬機使用到的Native方法提供內存空間。 有些虛擬機的實現直接把本地方法棧和虛擬機棧合二爲一,好比主流的HotSpot虛擬機。

    值得一提:在java虛擬機規範中,此處定義了兩個異常
    (1)StackOverFlowError (在遞歸中常看到,遞歸層級過深)
    (2)OutOfMemoryError
    網絡

  4. 方法區:主要存儲已加載是類信息(由ClassLoader加載)、常量、靜態變量、編譯後的代碼的一些信息。 GC在這裏比較少出如今這塊區域。多線程

  5. 堆:存放的是幾乎全部的對象實例和數組數據。 是虛擬機管理的最大的一塊內存,是GC的主戰場,因此也叫「GC堆」、「垃圾堆」 。

    值得一提:在java虛擬機規範中,此處定義了一個異常
    (1)OutOfMemoryError
    app

  6. 運行時常量池:屬於「方法區」的一部分,用於存放編譯器生成的各類字面量和符號引用。
    字面量:與Java語言層面的常量概念相近,包含文本字符串、聲明爲final的常量值等。
    符號引用:編譯語言層面的概念,包括如下3類:
    (1) 類和接口的全限定名
    (2)字段的名稱和描述符
    (3)方法的名稱和描述符
    eclipse

二、JAVA回收機制

java中是經過GC(Garbage Collection)來進行回收內存,那jvm是如何肯定一個對象可否被回收的呢?這裏就需講到其回收使用的算法

(1) 引用計數算法

引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每一個對象實例都有一個引用計數。當一個對象被建立時,且將該對象實例分配給一個變量,該變量計數設置爲1。當任何其它變量被賦值爲這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例能夠被看成垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1。

優勢:
  引用計數收集器能夠很快的執行,交織在程序運行中。對程序須要不被長時間打斷的實時環境比較有利。

缺點:
  沒法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0。例以下面代碼片斷中,最後的Object實例已經不在咱們的代碼可控範圍內,但其引用仍爲1,此時內存便產生泄漏

/**舉個例子**/
Object o1 = new Object()      //Object的引用+1,此時計數器爲1
Object o2;
o2.o  = o1;   			      //Object的引用+1,此時計數器爲2
o2 = null;
o1 = null;				      //Object的引用-1,此時計數器爲1
複製代碼

(2) 可達性分析算法

可達性分析算法

可達性分析算法是如今java的主流方法,經過一系列的GC ROOT爲起始點,從一個GC ROOT開始,尋找對應的引用節點,找到這個節點之後,繼續尋找這個節點的引用節點,當全部的引用節點尋找完畢以後,剩餘的節點則被認爲是沒有被引用到的節點,即無用的節點(即圖中的ObjD、ObjE、ObjF)。由此可知,即時引用成環也不會致使泄漏。

java中可做爲GC Root的對象有:
一、方法區中靜態屬性引用的對象
二、方法區中常量引用的對象
三、本地方法棧JNI中引用的對象(Native對象)
四、虛擬機棧(本地變量表)中正在運行使用的引用

可是,可達性分析算法中不可達的對象,也並不是必定要被回收。當GC第一次掃過這些對象的時候,他們處於「死緩」的階段。要真正執行死刑,至少須要通過兩次標記過程。 若是對象通過可達性分析以後發現沒有與GC Roots相關聯的引用鏈,那他會被第一次標記,並經歷一次篩選,這個對象的finalize方法會被執行。若是對象沒有覆蓋finalize或者已經被執行過了。虛擬機也不會去執行finalize方法。Finalize是對象逃獄的最後一次機會。

三、四種引用

說到底,內存泄漏是由於引用的處理不正當致使的。因此,咱們接下來須要老生常談一下java中四種引用,即:強軟弱虛(引用強度依次減弱)。

(1)強引用(Strong reference): 通常咱們使用的都是強引用,例如:Object o = new Object();只要強引用還在,垃圾收集器就不會回收被引用的對象。

(2)軟引用(Soft Reference): 用來定義一些還有用但並不是必須的對象。對於軟引用關聯着的對象,在系統將要內存溢出以前,會將這些對象列入回收範圍進行第二次回收,若是回收後仍是內存不足,纔會拋出內存溢出。(即在內存緊張時,會對其軟引用回收)

(3)弱引用(Weak Reference): 用來描述非必須對象。被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。當垃圾收集器回收時,不管內存是否足夠,都會回收掉被弱引用關聯的對象。(即GC掃過期,便將弱引用帶走)

(4)虛引用(Phantom Reference): 也稱爲幽靈引用或者幻影引用,是最弱的引用關係。一個對象的虛引用根本不影響其生存時間,也不能經過虛引用得到一個對象實例。 虛引用的惟一做用就是這個對象被GC時能夠收到一條系統通知。

軟引用與弱引用的抉擇
若是隻是想避免OutOfMemory異常的發生,則可使用軟引用。若是對於應用的性能更在乎,想盡快回收一些佔用內存比較大的對象,則可使用弱引用。另外能夠根據對象是否常用來判斷選擇軟引用仍是弱引用。若是該對象可能會常用的,就儘可能用軟引用。若是該對象不被使用的可能性更大些,就能夠用弱引用。

四、小結

至此,咱們知道內存泄漏是由於堆內存中的長生命週期的對象持有短生命週期對象的引用,儘管短生命週期對象已經再也不須要,可是由於長生命週期對象持有它的引用而致使不能被回收。

五、安卓內存泄漏排查工具

所謂工欲善其事必先利其器,這一小節先簡述下所需借用到的內存泄漏排查工具,若是已經熟悉的話能夠跳過。

(1) Android Profiler

這一工具是Android Studio自帶,能夠查看cpu、內存使用、網絡使用狀況,Android Studio3.0中用於替代Android Monitor

Android Profiler功能簡介
① 強制執行垃圾收集事件的按鈕。
② 捕獲堆轉儲的按鈕。
③ 記錄內存分配的按鈕。
④ 放大時間線的按鈕。
⑤ 跳轉到實時內存數據的按鈕。
⑥ 事件時間線顯示活動狀態、用戶輸入事件和屏幕旋轉事件。
⑦ 內存使用時間表,其中包括如下內容:
• 每一個內存類別使用多少內存的堆棧圖,如左邊的y軸和頂部的顏色鍵所示。
• 虛線表示已分配對象的數量,如右側y軸所示。
• 每一個垃圾收集事件的圖標。

(2) MAT(Memory Analyzer Tool)

MAT用於鎖定哪裏泄漏。由於從Android Profiler中,知道了泄漏,但比較難鎖定具體哪一個地方致使了泄漏,因此藉助MAT來鎖定,具體使用待會會藉助一個例子配合Android Profiler來介紹,稍安勿躁。

下載地址:www.eclipse.org/mat/downloa…

六、內存泄漏檢查與解決流程

通過前面的一段理論,可能不少小夥伴都有些不耐煩了,如今便來真正的操做。

舒適提示:理論是進階中必要的支持,不然只是知其然而不知其因此然

(1)第一步:對待檢測功能掃雷式操做

當咱們須要檢查一塊模塊,或是整個app哪一個地方有內存泄漏時,有時會比較茫然,有些大海撈針的感受,畢竟泄漏不是每一個頁面都會有,並且有時是一個功能纔會致使泄漏,因此咱們能夠採起「掃雷式操做」,也就是在須要檢查的頁面和功能中隨便先使用一番,舉個例子:假設檢查MainActivity泄漏狀況,能夠登陸進入後,此時來到了MainActivity,後又登出,再次登陸進入MainActivity。

(2)第二步:藉助 Android Profiler得到內存快照

使用Android Profiler的GC功能,強制進行垃圾回收,再dump下內存("Android Profiler功能簡介"圖的②按鈕)。而後等待一段時間,會出現圖中紅色框部分:

在這裏獲得的頁面,其實比較難直觀得到內存分析的數據,最多隻是選擇「Arrange by package」按照包進行排序,而後進到本身的包下,查看應用內的activity的引用數是否正常,來判斷其是否有正常回收

圖中列的說明
Alloc Cout : 對象數
Shallow Size : 對象佔用內存大小
Retained Set : 對象引用組佔用內存大小(包含了這個對象引用的其餘對象)

(3)第三步:藉助Android Studio分析

至此,咱們仍是沒獲得直觀的內存分析數據,咱們須要藉助更專業的工具。咱們現將經過下圖中紅框內的按鈕,將剛纔的內存快照保存爲hprof文件。

將保存好的hprof文件拖進AS中,勾選「Detect Leaked Activities」,而後點擊綠色按鈕進行分析。
若是有內存泄漏的話,會出現以下圖的狀況。圖中很清晰的能夠看到,這裏出現了MainActivity的泄漏。而且觀察到這個MainActivity可能不止一個對象存在,多是咱們上次退出程序的時候發生了泄漏,致使它不能回收。而在此打開app,系統會建立新的MainActivity。但至此咱們只是知道MainActivity泄漏了,不知具體是哪裏致使了MainActivity泄漏,因此須要藉助MAT來進一步分析。
(4)第四步:hprof文件轉換

在使用MAT打開hprof文件前先要對剛纔保存的hprof文件進行轉換。經過終端,藉助轉換工具hprof-conv(在sdk/platform-tools/hprof-conv),使用命令行:

hprof-conv -z src dst
複製代碼

-z:排除不是app的內存,好比Zygote
src:須要進行轉換的hprof的文件路徑
dst:轉換後的文件路徑(文件後綴仍是.hprof)

(5)第五步:經過MAT進行具體分析 在MAT中打開轉換了的hprof文件,以下圖

打開後會看到以下圖
咱們須要進入到"Histogram"來分析,點擊下圖中的按鈕
打開"Histogram"後,會看到下圖,在紅框中輸入在AS中觀察到的泄漏的類,例如上面得知的MainActivity
而後將搜索獲得的結果進行合併,排除「軟」、「弱」、「虛」引用對象,右鍵點擊搜索到的結果,選擇以下圖的選項
獲得合併結果以下
從分析結果可知,MainActivity是由於com.netease.nimlib.g.e中的一個hashMap持有致使,這裏的e類是第三方庫的類,顯然已被混淆,形成泄漏無非兩種可能,一種是第三方庫的bug,一種是本身使用不當,例如忘記解綁操做等。具體的打斷這個持有須要按照本身的代碼進行分析,實例中的問題是由於使用第三方庫註冊後,在退出頁面沒有進行註銷致使的。

當咱們解決完後,能夠再次進行一輪內存快照,直到沒有內存泄漏,過程會比較枯燥,但一點點的解決泄漏最終會給app一個質的飛躍。

七、常見的內存泄漏緣由

(1)集合類

集合類若是僅僅有添加元素的方法,而沒有相應的刪除機制,致使內存被佔用。若是這個集合類是全局性的變量 (好比類中的靜態屬性,全局性的 map 等即有靜態引用或 final 一直指向它),那麼沒有相應的刪除機制,極可能致使集合所佔用的內存只增不減。

(2)單例模式

不正確使用單例模式是引發內存泄露的一個常見問題,單例對象在被初始化後將在 JVM 的整個生命週期中存在(以靜態變量的方式),若是單例對象持有外部對象的引用,那麼這個外部對象將不能被 JVM 正常回收,致使內存泄露。

public class SingleTest{
      private static SingleTest instance;
      private Context context;
      private SingleTest(Context context){
          this.context = context;
      }
      public static SingleTest getInstance(Context context){
          if(instance != null){
                instance = new SingleTest(context);
          }
          return instance;
      }
}
複製代碼

這裏若是傳遞Activity做爲Context來得到單例對象,那麼單例持有Activity的引用,致使Activity不能被釋放。 不要直接對 Activity 進行直接引用做爲成員變量,若是容許可使用Application。 若是不得不須要Activity做爲Context,可使用弱引用WeakReference,相同的,對於Service 等其餘有本身生命週期的對象來講,直接引用都須要謹慎考慮是否會存在內存泄露的可能。

(3)未關閉或釋放資源

BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某類生命週期結束以後必定要 unregister 或者 close 掉,不然這個 Activity 類會被 system 強引用,不會被內存回收。值得注意的是,關閉的語句必須在finally中進行關閉,不然有可能由於異常未關閉資源,導致activity泄漏

(4)Handler

只要 Handler 發送的 Message 還沒有被處理,則該 Message 及發送它的 Handler 對象將被線程 MessageQueue 一直持有。特別是handler執行延遲任務。因此,Handler 的使用要尤其當心,不然將很容易致使內存泄露的發生。

public class MainActivity extends AppCompatActivity {
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            //do something
        }
    };
    private void loadData(){
        //do request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
}
複製代碼

這種建立Handler的方式會形成內存泄漏,因爲mHandler是Handler的非靜態匿名內部類的實例,因此它持有外部類Activity的引用,咱們知道消息隊列是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,因此致使該Activity的內存資源沒法及時回收,引起內存泄漏,因此另一種作法爲:

public class MainActivity extends AppCompatActivity {
    private MyHandler mHandler = new MyHandler(this);
    private void loadData() {
        //do request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<Context>(context);
        }
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity mainActivity = (MainActivity) reference.get();
            if (mainActivity != null) {
                //do something to update UI via mainActivity
            }
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }
}
複製代碼

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

@Override
protected void onDestroy() {
    super.onDestroy();
    mHandler.removeCallbacksAndMessages(null);
}
複製代碼

使用mHandler.removeCallbacksAndMessages(null);是移除消息隊列中全部消息和全部的Runnable。固然也可使用mHandler.removeCallbacks();或mHandler.removeMessages();來移除指定的Runnable和Message。

(5)Thread

和handler同樣,線程也是形成內存泄露的一個重要的源頭。線程產生內存泄露的主要緣由在於線程生命週期的不可控。好比線程是 Activity 的內部類,則線程對象中保存了 Activity 的一個引用,當線程的 run 函數耗時較長沒有結束時,線程對象是不會被銷燬的,所以它所引用的老的 Activity 也不會被銷燬,所以就出現了內存泄露的問題。

(6)系統bug

好比InputMethodManager,會持有activity而沒釋放,致使泄漏,須要經過反射進行打斷。

若是須要更多的交流與探討,能夠經過如下微信二維碼加小盆友好友。

相關文章
相關標籤/搜索