Android性能優化之內存泄漏

在Android開發的過程中,經常需要注意內存泄漏問題,不然很容易導致OOM問題,或者因此引起頻繁gc造成app卡頓。 下面這篇文章將分析內存泄漏的原因、Android內存管理的相關內容,并分享一些檢測泄漏的方法和如何避免內存泄漏。

1. 內存泄漏的定義

Android是基于Java的,眾所周知Java語言的內存管理是其一大特點, 不用像C語言那樣處理對象的內存分配到回收的全部過程。在Java中我們只需要簡單地新建對象就可以了, Java垃圾回收器會負責回收釋放對象內存。 這么看的話,垃圾回收器會管理內存又怎么還會發生內存泄漏呢?

其實Java中的內存泄漏的定義是: 對象不再被程序所使用, 但是由于這些對象被引用著導致GC(Garbage Collector)不能回收它們。

下面這張圖可以幫助我們更好地理解對象的狀態,以及內存泄漏的情況

左邊未引用的對象是會被GC回收的,右邊被引用的對象不會被GC回收,但是未使用的對象中除了未引用的對象,還包括已被引用的一部分對象,那么內存泄漏久發生這部分已被引用但未使用的對象。

接下來還有一個疑問:未使用的對象被誰引用會讓GC無法回收呢?

現在主流的程序語言的主流實現中, 是通過可達性分析(Reachability Analysis)來判斷對象是否存活的。這個算法的基本思路是:通過一系列的稱為“GC Roots”的對象作為起點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈時,說明此對象不可用,可以被回收了。

可以作為GC Roots的對象包括下面幾種:

  • 虛擬機棧中引用的對象, 一般是當前在使用中局部變量

  • 方法區中類靜態屬性引用的對象, 就是靜態變量對應的對象

  • 方法區中常量引用的對象

  • 本地方法棧中JNI(即一般說的Native方法)引用的對象

MAT分析內存泄漏的時候,也是查看對象到GC Roots的引用鏈,來定位泄漏代碼的位置。

所以未使用的對象直接或間接地被GC Roots引用時會讓GC無法回收,從而產生內存泄漏。

2. Android的內存管理

了解了Java的內存泄漏的起因,接下來大致了解Android中的內存管理機制。

Google在Android的官網上有這樣一篇文章,初步介紹了Android是如何管理應用的進程與內存分配:http://developer.android.com/training/articles/memory.html。 Android系統的Dalvik虛擬機扮演了常規的內存垃圾自動回收的角色,Android系統沒有為內存提供交換區,它使用 pagingmemory-mapping(mmapping) 的機制來管理內存,下面簡要概述一些Android系統中重要的內存管理基礎概念。

分配與回收內存

每一個進程的Dalvik heap都反映了使用內存的占用范圍。這就是通常邏輯意義上提到的Dalvik Heap Size,它可以隨著需要進行增長,但是增長行為會有一個系統為它設定的上限。

邏輯上講的Heap Size和實際物理意義上使用的內存大小是不對等的,Proportional Set Size(PSS)記錄了應用程序自身占用以及和其他進程進行共享的內存。

Android系統并不會對Heap中空閑內存區域做碎片整理。系統僅僅會在新的內存分配之前判斷Heap的尾端剩余空間是否足夠,如果空間不夠會觸發gc操作,從而騰出更多空閑的內存空間。在Android的高級系統版本里面針對Heap空間有一個Generational Heap Memory的模型,最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最后累積一定時間再移動到Permanent Generation區域。系統會根據內存中不同的內存數據類型分別執行不同的gc操作。例如,剛分配到Young Generation區域的對象通常更容易被銷毀回收,同時在Young Generation區域的gc操作速度會比Old Generation區域的gc操作速度更快。如下圖所示:

每一個Generation的內存區域都有固定的大小,隨著新的對象陸續被分配到此區域,當這些對象總的大小快達到這一級別內存區域的閥值時,會觸發GC的操作,以便騰出空間來存放其他新的對象。如下圖所示:

通常情況下,GC發生的時候,所有的線程都是會被暫停的。執行GC所占用的時間和它發生在哪一個Generation也有關系,Young Generation中的每次GC操作時間是最短的,Old Generation其次,Permanent Generation最長。執行時間的長短也和當前Generation中的對象數量有關,遍歷樹結構查找20000個對象比起遍歷50個對象自然是要慢很多的。

為什么通常情況下,GC發生的時候,所有的線程都會被暫停?

因為每次GC的時候,需要先找到可作為GC Roots的對象,然后以此搜索引用鏈,這個過程需要在一致性的內存快照中進行。這個“一致性”表示在整個過程中不能出現對象引用關系不斷變化的情況,所以需要暫停所有的執行線程。

限制應用的內存

為了整個Android系統的內存控制需要,Android系統為每一個應用程序都設置了一個硬性的Dalvik Heap Size最大限制閾值,這個閾值在不同的設備上會因為RAM大小不同而各有差異。如果你的應用占用內存空間已經接近這個閾值,此時再嘗試分配內存的話,很容易引起OutOfMemoryError的錯誤。

ActivityManager.getMemoryClass()可以用來查詢當前應用的Heap Size閾值,這個方法會返回一個整數,表明你的應用的Heap Size閾值是多少Mb(megabates)。

還有一個用adb命令查詢的方法:

adb shell getprop dalvik.vm.heapgrowthlimit

3. 檢測與定位內存泄漏

(1)adb命令

adb shell dumpsys meminfo {package_name}

(2)Android Studio的Memory Monitor

(3)LeakCanary

(4)MAT

在Android檢查內存泄漏,主要搜索Activity、Fragment、View有沒有泄漏。

4. 如何避免內存的總結

(1) 注意Activity的泄漏

  • 內部類引用導致Activity泄漏

具體見 Android中由Handler和內部類引起的內存泄

  • Activity Context被間接引用

對于大部分非必須使用Activity Context的情況(Dialog的Context就必須是Activity Context),我們都可以考慮使用Application Context而不是Activity的Context,這樣可以避免不經意的Activity泄露

(2) 注意靜態變量和單例模式

靜態變量是作為GC Roots,在Android其生命周期基本和進程一樣長,所以要非常靜態變量引用其他生命周期的對象。雖然單例模式簡單實用,提供了很多便利性,但是因為單例的生命周期和應用保持一致,使用不合理很容易出現持有對象的泄漏。

(3) 注意容器中對象泄漏

有時候,我們為了提高對象的復用性把某些對象放到緩存容器中,可是如果這些對象沒有及時從容器中清除,也是有可能導致內存泄漏的。例如,針對2.3的系統,如果把drawable添加到緩存容器,因為drawable與View的強應用,很容易導致activity發生泄漏。而從4.0開始,就不存在這個問題。解決這個問題,需要對2.3系統上的緩存drawable做特殊封裝,處理引用解綁的問題,避免泄漏的情況。

(4) 注意監聽器的注銷

(5) …

(6) 及時關閉Cursor

在程序中我們經常會進行查詢數據庫的操作,但時常會存在不小心使用Cursor之后沒有及時關閉的情況。這些Cursor的泄露,反復多次出現的話會對內存管理產生很大的負面影響,我們需要謹記對Cursor對象的及時關閉。

 

來自:http://johnnyshieh.github.io/android/2016/11/18/android-memory-leak/

 

 本文由用戶 MildredJean 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!