系統剖析Android中的內存泄漏
作爲Android開發人員,我們或多或少都聽說過內存泄漏。那麼何爲內存泄漏,Android中的內存泄漏又是什麼樣子的呢,本文將簡單概括的進行一些總結。
關於內存泄露的定義,我可以理解成這樣
沒有用的對象無法回收的現象就是內存泄露
如果程序發生了內存泄露,則會帶來如下的問題
- 應用可用的內存減少,增加了堆內存的壓力
- 降低了應用的性能,比如會觸犯更頻繁的GC
- 嚴重的時候可能會導致內存溢出錯誤,即OOM Error
在正式介紹內存泄露之前,我們有必要介紹一些必要的預備知識。
預備知識1: Java中的對象
-
當我們使用
new
指令生成對象時,堆內存將會爲此開闢一份空間存放該對象 - 創建的對象可以被局部變量,實例變量和類變量引用。
- 通常情況下,類變量持有的對象生命週期最長,實例變量次之,局部變量最短。
- 垃圾回收器回收非存活的對象,並釋放對應的內存空間。
預備知識2:Java中的GC
- 和C++不同,對象的釋放不需要手動完成,而是由垃圾回收器自動完成。
- 垃圾回收器運行在JVM中
- 通常GC有兩種算法:引用計數和GC根節點遍歷
引用計數
- 每個對象有對應的引用計數器
- 當一個對象被引用(被複制給變量,傳入方法中),引用計數器加1
- 當一個對象不被引用(離開變量作用域),引用計數器就會減1
- 基於這種算法的垃圾回收器效率較高
- 循環引用的問題引用計數算法的垃圾回收器無法解決。
- 主流的JVM很少使用基於這種算法的垃圾回收器實現。
GC根節點遍歷
- 識別對象爲垃圾從被稱爲GC 根節點出發
- 每一個被遍歷的強引用可到達對象,都會被標記爲存活
- 在遍歷結束後,沒有被標記爲存活的對象都被視爲垃圾,需要後續進行回收處理
- 主流的JVM一般都採用這種算法的垃圾回收器實現
以上圖爲例,我們可以知道
- 最下層的兩個節點爲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的類,它是一個單例模式的應用。
當我們傳入Activity作爲Context參數時,則AppSettings實例會持有這個Activity的實例。
當我們旋轉設備時,Android系統會銷燬當前的Activity,創建新的Activity來加載合適的佈局。如果出現Activity被單例實例持有,那麼旋轉過程中的舊Activity無法被銷燬掉。就發生了我們所說的內存泄漏。
想要解決這個問題也不難,那就是使用Application的Context對象,因爲它和AppSettings實例具有相同的生命週期。這裏是通過使用Context.getApplicationContext()
方法來實現。所以修改如下
忘記反註冊監聽器
在Android中我們會使用很多listener,observer。這些都是作爲觀察者模式的實現。當我們註冊一個listener時,這個listener的實例會被主題所引用。如果主題的生命週期要明顯大於listener,那麼就有可能發生內存泄漏。
以下面的代碼爲例
上述代碼處理的業務,可以理解爲
- AppCompatActivity實現了OnNetworkChangedListener接口,用來監聽網絡的可用性變化
- NetworkManager爲單例模式實現,其registerListener接收了MainActivity實例
又是單例模式,可知NetworkManager會持有MainActivity的實例引用,因而屏幕旋轉時,MainActivity同樣無法被回收,進而造成了內存泄漏。
對於這種類型的內存泄漏,解決方法是這樣的。即在MainActivity的onDestroy方法中加入反註銷的方法調用。
非靜態內部類導致的內存泄漏
在Java中,非靜態內部類會隱式持有外部類的實例引用。想要了解更多,可以參考這篇文章細話Java:”失效”的private修飾符
通常情況下,我們會書寫類似這樣的代碼
其中上面的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中,用於展示應用內存的使用和釋放情況。它大致長成這個樣子
當你的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的佔用內存大小。
以上圖做個例子,進行分析
- 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最近的統治者。
上圖中
- 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官方網站