系統剖析Android中的內存泄漏

系統剖析Android中的內存泄漏

作爲Android開發人員,我們或多或少都聽說過內存泄漏。那麼何爲內存泄漏,Android中的內存泄漏又是什麼樣子的呢,本文將簡單概括的進行一些總結。

關於內存泄露的定義,我可以理解成這樣

沒有用的對象無法回收的現象就是內存泄露

如果程序發生了內存泄露,則會帶來如下的問題

  • 應用可用的內存減少,增加了堆內存的壓力
  • 降低了應用的性能,比如會觸犯更頻繁的GC
  • 嚴重的時候可能會導致內存溢出錯誤,即OOM Error

在正式介紹內存泄露之前,我們有必要介紹一些必要的預備知識。

預備知識1: Java中的對象

  • 當我們使用new指令生成對象時,堆內存將會爲此開闢一份空間存放該對象
  • 創建的對象可以被局部變量,實例變量和類變量引用。
  • 通常情況下,類變量持有的對象生命週期最長,實例變量次之,局部變量最短。
  • 垃圾回收器回收非存活的對象,並釋放對應的內存空間。

預備知識2:Java中的GC

  • 和C++不同,對象的釋放不需要手動完成,而是由垃圾回收器自動完成。
  • 垃圾回收器運行在JVM中
  • 通常GC有兩種算法:引用計數和GC根節點遍歷

引用計數

  • 每個對象有對應的引用計數器
  • 當一個對象被引用(被複制給變量,傳入方法中),引用計數器加1
  • 當一個對象不被引用(離開變量作用域),引用計數器就會減1
  • 基於這種算法的垃圾回收器效率較高
  • 循環引用的問題引用計數算法的垃圾回收器無法解決。
  • 主流的JVM很少使用基於這種算法的垃圾回收器實現。

GC根節點遍歷

  • 識別對象爲垃圾從被稱爲GC 根節點出發
  • 每一個被遍歷的強引用可到達對象,都會被標記爲存活
  • 在遍歷結束後,沒有被標記爲存活的對象都被視爲垃圾,需要後續進行回收處理
  • 主流的JVM一般都採用這種算法的垃圾回收器實現

http://7xuvjz.com1.z0.glb.clouddn.com/how_gc_works.png

以上圖爲例,我們可以知道

  • 最下層的兩個節點爲GC Roots,即GC Tracing的起點
  • 中間的一層的對象,可以強引用到達GC根節點,所以被標記爲存活
  • 最上層的三個對象,無法強引用達到GC根節點,所以無法標記爲存活,也就是所謂的垃圾,需要被後續回收掉。

上面的垃圾回收中,我們提到的兩個概念,一個是GC根節點,另一個是強引用

在Java中,可以作爲GC 根節點的有

  • 類,由系統類加載器加載的類。這些類從不會被卸載,它們可以通過靜態屬性的方式持有對象的引用。注意,一般情況下由自定義的類加載器加載的類不能成爲GC Roots
  • 線程,存活的線程
  • Java方法棧中的局部變量或者參數
  • JNI方法棧中的局部變量或者參數
  • JNI全局引用
  • 用做同步監控的對象
  • 被JVM持有的對象,這些對象由於特殊的目的不被GC回收。這些對象可能是系統的類加載器,一些重要的異常處理類,一些爲處理異常預留的對象,以及一些正在執行類加載的自定義的類加載器。但是具體有哪些前面提到的對象依賴於具體的JVM實現。

提到強引用,有必要系統說一下Java中的引用類型。Java中的引用類型可以分爲一下四種:

  • 強引用: 默認的引用類型,例如StringBuffer buffer = new StringBuffer();就是buffer變量持有的爲StringBuilder的強引用類型。
  • 軟引用:即SoftReference,其指向的對象只有在內存不足的時候進行回收。
  • 弱引用:即WeakReference,其指向的對象在GC執行時會被回收。
  • 虛引用:即PhantomReference,與ReferenceQueue結合,用作記錄該引用指向的對象已被銷燬。

補充了預備知識,我們就需要具體講一講Android中的內存泄漏了。

Android中的內存泄漏

歸納而言,Android中的內存泄漏有以下幾個特點:

  • 相對而言,Android中的內存泄漏更加容易出現。
  • 由於Android系統爲每個App分配的內存空間有限,在一個內存泄漏嚴重的App中,很容易導致OOM,即內存溢出錯誤。
  • 內存泄漏會隨着App的推出而消失(即進程結束)。

在Android中的內存泄漏場景有很多,按照類型劃分可以歸納爲

  • 長期持有(Activity)Context導致的
  • 忘記註銷監聽器或者觀察者
  • 由非靜態內部類導致的

此外,如果按照泄漏的程度,可以分爲

  • 長時間泄漏,即泄漏只能等待進程退出才消失
  • 短時間泄漏,被泄漏的對象後續會被回收掉。

長時間持有Activity實例

在Android中,Activity是我們常用的組件,通常情況下,一個Activity會包含了一些複雜的UI視圖,而視圖中如果含有ImageView,則有可能會使用比較大的Bitmap對象。因而一個Activity持有的內存會相對很多,如果造成了Activity的泄漏,勢必造成一大塊內存無法回收,發生泄漏。

這裏舉個簡單的例子,說明Activity的內存泄漏,比如我們有一個叫做AppSettings的類,它是一個單例模式的應用。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AppSettings {
    private Context mAppContext;
    private static AppSettings sInstance = new AppSettings();

    //some other codes
    public static AppSettings getInstance() {
      return sInstance;
    }

    public final void setup(Context context) {
        mAppContext = context;
    }
}

當我們傳入Activity作爲Context參數時,則AppSettings實例會持有這個Activity的實例。

當我們旋轉設備時,Android系統會銷燬當前的Activity,創建新的Activity來加載合適的佈局。如果出現Activity被單例實例持有,那麼旋轉過程中的舊Activity無法被銷燬掉。就發生了我們所說的內存泄漏。

想要解決這個問題也不難,那就是使用Application的Context對象,因爲它和AppSettings實例具有相同的生命週期。這裏是通過使用Context.getApplicationContext()方法來實現。所以修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AppSettings {
    private Context mAppContext;
    private static AppSettings sInstance = new AppSettings();

    //some other codes
    public static AppSettings getInstance() {
      return sInstance;
    }

    public final void setup(Context context) {
        mAppContext = context.getApplicationContext();
    }
}

忘記反註冊監聽器

在Android中我們會使用很多listener,observer。這些都是作爲觀察者模式的實現。當我們註冊一個listener時,這個listener的實例會被主題所引用。如果主題的生命週期要明顯大於listener,那麼就有可能發生內存泄漏。

以下面的代碼爲例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }
}

上述代碼處理的業務,可以理解爲

  • AppCompatActivity實現了OnNetworkChangedListener接口,用來監聽網絡的可用性變化
  • NetworkManager爲單例模式實現,其registerListener接收了MainActivity實例

又是單例模式,可知NetworkManager會持有MainActivity的實例引用,因而屏幕旋轉時,MainActivity同樣無法被回收,進而造成了內存泄漏。

對於這種類型的內存泄漏,解決方法是這樣的。即在MainActivity的onDestroy方法中加入反註銷的方法調用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        NetworkManager.getInstance().unregisterListener(this);
    }

}

非靜態內部類導致的內存泄漏

在Java中,非靜態內部類會隱式持有外部類的實例引用。想要了解更多,可以參考這篇文章細話Java:”失效”的private修飾符

通常情況下,我們會書寫類似這樣的代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SensorListenerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SensorManager sensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
        sensorManager.registerListener(new SensorListener() {
            @Override
            public void onSensorChanged(int sensor, float[] values) {

            }

            @Override
            public void onAccuracyChanged(int sensor, int accuracy) {

            }
        }, SensorManager.SENSOR_ALL);
    }
}

其中上面的SensorListner實例是一個匿名內部類的實例,也是非靜態內部類的一種。因此SensorListner也會持有外部SensorListenerActivity的實例引用。

而SensorManager作爲單例模式實現,其生命週期與Application相同,和SensorListner對象生命週期不同,有可能間接導致SensorListenerActivity發生內存泄漏。

解決這種問題的方法可以是

  • 使用實例變量存儲SensonListener實例,在Activity的onDestroy方法進行反註冊。
  • 如果registerListener方法可以修改,可以使用弱引用或者WeakHashMap來解決。

除了上面的三種場景外,Android的內存泄漏還有可能出現在以下情況

  • 使用Activity.getSystemService()使用不當,也會導致內存泄漏。
  • 資源未關閉也會造成內存泄漏
  • Handler使用不當也可以造成內存泄漏的發生
  • 延遲的任務也可能導致內存泄漏

解決內存泄漏

想要解決內存泄漏無非如下兩種方法

  • 手動解除不必要的強引用關係
  • 使用弱引用或者軟引用替換強引用關係

下面會簡單介紹一些內存泄漏檢測和解決的工具

Strictmode

  • StrictMode,嚴格模式,是Android中的一種檢測VM和線程違例的工具。
  • 使用detectAll()或者detectActivityLeaks()可以檢測Activity的內存泄漏
  • 使用setClassInstanceLimit()可以限定類的實例個數,可以輔助判斷某些類是否發生了內存泄漏
  • 但是StrictMode只能檢測出現象,並不能提供更多具體的信息。
  • 瞭解更多關於StrictMode,請訪問Android性能調優利器StrictMode

Android Memory Monitors

Android Memory Monitor內置於Android Studio中,用於展示應用內存的使用和釋放情況。它大致長成這個樣子

http://7xuvjz.com1.z0.glb.clouddn.com/heap_monitor.gif

當你的App佔用的內存持續增加,而且你同時出發GC,也沒有進行釋放,那麼你的App很有可能發生了內存泄漏問題。

LeakCanary

  • LeakCanary是一個檢測Java和Android內存泄漏的庫
  • 由Square公司開發
  • 集成LeakCanary之後,只需要等待內存泄漏出現就可以了,無需認爲進行主動檢測。
  • 關於如何使用LeakCanary,可以參考這篇文章 Android內存泄漏檢測利器:LeakCanary

Heap Dump

  • 一個Heap dump就是某一時間點的內存快照
  • 它包含了某個時間點的Java對象和類信息。
  • 我們可以通上述提到的Android Heap Monitor進行Heap Dump,當然LeakCanary也會生成Heap Dump文件。
  • 生成的Heap Dump文件擴展名爲.hprof 即Heap Profile.
  • 通常情況下,一個heap profile需要轉換後才能被MAT使用分析。

Shallow Heap VS Retained Heap

  • Shallow Heap 指的是對象自身的佔用的內存大小。
  • 對象x的Retained Set指的是如果對象x被GC移除,可以釋放總的對象的集合。
  • 對象x的Retained Heap指的就是上述x的Retained Set的佔用內存大小。

http://7jpolu.com1.z0.glb.clouddn.com/shallow_heap_retained_heap.png

以上圖做個例子,進行分析

  • A,B,C,D四個對象的Shallow Heap均爲1M
  • B,C,D的Retained Heap均爲1M
  • A的Retained Heap爲4M

真實情況下如何計算泄漏內存大小

上述的Retained Heap的大小獲取是基於假設的,而現實在進行分析中不可能基於這種方法,那麼實際上計算泄漏內存的大小的方法其實是這樣的。

這裏我們需要一個概念,就是Dominator Tree(統治者樹)。

  • 如果對象x統治對象y,那麼每條從GC根節點到y對象的路徑都會經過x,即x是GC根節點到y的必經之路。
  • 上述情況下,我們可以說x是y的統治者
  • 最近統治者指的是離對象y最近的統治者。

http://7jpolu.com1.z0.glb.clouddn.com/dominator_tree.png

上圖中

  • A和B都不無法統治C對象,即C對象被A和B的父對象統治
  • H不受F,G,D,E統治,但是受C統治
  • F和D是循環引用,但是按照路徑的方向(從根節點到對象),D統治F

內存泄漏與OOM

  • OOM全稱Out Of Memory Error 內存溢出錯誤
  • OOM發生在,當我們嘗試進行創建對象,但是堆內存無法通過GC釋放足夠的空間,堆內存也無法在繼續增長,從而完成對象創建請求,所以發生了OOM
  • OOM發生很有可能是內存泄漏導致
  • 但是並非所有的OOM都是由內存泄漏引起
  • 內存泄漏也並不一定引起OOM

聲明

  • 其中第一張圖片GC回收圖來自Patrick Dubroy在Google IO的演講Keynote
  • 最後一張Dorminator Tree來自MAT官方網站
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章