Android 內存泄露總結

TangelaPerr 8年前發布 | 5K 次閱讀 安卓開發 Android開發 移動開發

Java 中的內存分配

主要是分三塊:

  • 靜態儲存區:編譯時就分配好,在程序整個運行期間都存在。它主要存放靜態數據和常量。

  • 棧區:當方法執行時,會在棧區內存中創建方法體內部的局部變量,方法結束后自動釋放內存。

  • 堆區:通常存放 new 出來的對象。由 Java 垃圾回收器回收。

棧與堆的區別

棧內存用來存放局部變量和函數參數等。它是先進后出的隊列,進出一一對應,不產生碎片,運行效率穩定高。當超過變量的作用域后,該變量也就無效了,分配給它的內存空間也將被釋放掉,該內存空間可以被重新使用。

堆內存用于存放對象實例。在堆中分配的內存,將由Java垃圾回收器來自動管理。在堆內存中頻繁的 new/delete 會造成大量內存碎片,使程序效率降低。

對于非靜態變量的儲存位置,我們可以粗暴的認為:

  • 局部變量位于棧中(其中引用變量指向的對象實體存在于堆中)。

  • 成員變量位于堆中。因為它們屬于類,該類最終被new成對象,并作為一個整體儲存在堆中。

四種引用類型的介紹

GC 釋放對象的根本原則是該對象不再被引用(強引用)。那么什么是強引用呢?

強引用(Strong Reference)

我們平常用的最多的就是強引用,如下:

IPhotos iPhotos = new IPhotos();

JVM 寧可拋出 OOM ,也不會讓 GC 回收具有強引用的對象。強引用不使用時,可以通過 obj = null 來顯式的設置該對象的所有引用為 null,這樣就可以回收該對象了。至于什么時候回收,取決于 GC 的算法,這里不做深究。

軟引用(Soft Reference)

SoftReference<String> softReference = new SoftReference<>(str);

如果一個對象只具有軟引用,那么在內存空間足夠時,垃圾回收器就不會回收它;如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被使用。

軟引用曾經常被用來作圖片緩存,然而谷歌現在推薦用 LruCache 替代,因為 LRU 更高效。

In the past, a popular memory cache implementation was a SoftReference

or WeakReference bitmap cache, however this is not recommended.

Starting from Android 2.3 (API Level 9) the garbage collector is more

aggressive with collecting soft/weak references which makes them

fairly ineffective. In addition, prior to Android 3.0 (API Level 11),

the backing data of a bitmap was stored in native memory which is not

released in a predictable manner, potentially causing an application

原文

大致意思是:因為在 Android 2.3 以后,GC 會很頻繁,導致釋放軟引用的頻率也很高,這樣會降低它的使用效率。并且 3.0 以前 Bitmap 是存放在 Native Memory 中,它的釋放是不受 GC 控制的,所以使用軟引用緩存 Bitmap 可能會造成 OOM。

弱引用(Weak Reference)

WeakReference<String> weakReference = new WeakReference<>(str);

與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。因為在 GC 時,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程,因此不一定會很快發現那些只具有弱引用的對象- -。

虛引用(PhantomReference)

顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期,也無法通過虛引用獲得對象實例。虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否存在該對象的虛引用,來了解這個對象是否將要被回收。

Android的垃圾回收機制簡介

Android 系統里面有一個 Generational Heap Memory 模型,系統會根據內存中不同的內存數據類型分別執行不同的 GC 操作。

該模型分為三個區:

  • Young Generation

    1. eden

    2. Survivor Space

      1. S0

      2. S1

  • Old Generation

  • Permanent Generation

Young Generation

大多數 new 出來的對象都放到 eden 區,當 eden 區填滿時,執行 Minor GC(輕量級GC),然后存活下來的對象被轉移到 Survivor 區(有 S0,S1 兩個)。 Minor GC 也會檢查 Survivor 區的對象,并把它們轉移到另一個 Survivor 區,這樣就總會有一個 Survivor 區是空的。

Old Generation

存放長期存活下來的對象(經過多次 Minor GC 后仍然存活下來的對象) Old Generation 區滿了以后,執行 Major GC(大型 GC)。

在Android 2.2 之前,執行 GC 時,應用的線程會被暫停,2.3 開始添加了并發垃圾回收機制。

Permanent Generation

存放方法區。一般存放:

  • 要加載的類的信息

  • 靜態變量

  • final常量

  • 屬性、方法信息

60 FPS

這里簡單的介紹一下幀率的概念,以便于理解為什么大量的 GC 容易引起卡頓。

App 開發時,一般追求界面的幀率達到60 FPS(60 幀/秒),那這個 FPS 是什么概念呢?

  • 10-12 FPS 時可以感受到動畫的效果;

  • 24 FPS,可以感受到平滑連貫的動畫效果,電影常用幀率(不追求 60 FPS 是節省成本);

  • 60 FPS,達到最流暢的效果,對于更高的FPS,大腦已經難以察覺區別。

Android 每隔 16 ms發出 VSYNC 信號,觸發對 UI 的渲染(即每 16 ms繪制一幀),如果整個過程保持在 16 ms以內,那么就會達到 60 FPS 的流暢畫面。超過了 16 ms就會造成卡頓。那么如果在 UI 渲染時發生了大量 GC,或者 GC 耗時太長,那么就可能導致繪制過程超過 16 ms從而造成卡頓(FPS 下降、掉幀等),而我們大腦對于掉幀的情況十分敏銳,因此如果沒有做好內存管理,將會給用戶帶來非常不好的體驗。

再介紹一下內存抖動的概念,本文后面可能會用到這個概念。

內存抖動

短時間內大量 new 對象,達到 Young Generation 的閾值后觸發GC,導致剛 new 出來的對象又被回收。此現象會影響幀率,造成卡頓。

內存抖動在 Android 提供的 Memory Monitor 中大概表現為這樣:

Android中常見的內存泄露及解決方案

集合類泄露

如果某個集合是全局性的變量(比如 static 修飾),集合內直接存放一些占用大量內存的對象(而不是通過弱引用存放),那么隨著集合 size 的增大,會導致內存占用不斷上升,而在 Activity 等銷毀時,集合中的這些對象無法被回收,導致內存泄露。比如我們喜歡通過靜態 HashMap 做一些緩存之類的事,這種情況要小心,集合內對象建議采用弱引用的方式存取,并考慮在不需要的時候手動釋放。

單例造成的內存泄露

單例的靜態特性導致其生命周期同應用一樣長。

有時創建單例時如果我們需要Context對象,如果傳入的是Application的Context那么不會有問題。如果傳入的是Activity的Context對象,那么當Activity生命周期結束時,該Activity的引用依然被單例持有,所以不會被回收,而單例的生命周期又是跟應用一樣長,所以這就造成了內存泄露。

解決辦法一:在創建單例的構造中不直接用傳進來的context,而是通過這個context獲取Application的Context。代碼如下:

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context.getApplicationContext();// 使用Application 的context
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

第二種解決方案:在構造單例時不需要傳入 context,直接在我們的 Application 中寫一個靜態方法,方法內通過 getApplicationContext 返回 context,然后在單例中直接調用這個靜態方法獲取 context。

非靜態內部類造成的內存泄露

在 Java 中,非靜態內部類(包括匿名內部類,比如 Handler, Runnable匿名內部類最容易導致內存泄露)會持有外部類對象的強引用(如 Activity),而靜態的內部類則不會引用外部類對象。

非靜態內部類或匿名類因為持有外部類的引用,所以可以訪問外部類的資源屬性成員變量等;靜態內部類不行。

因為普通內部類或匿名類依賴外部類,所以必須先創建外部類,再創建普通內部類或匿名類;而靜態內部類隨時都可以在其他外部類中創建。

Handler內存泄露可以關注我的另一篇專門針對Handler內存泄露的文章:鏈接

WebView 的泄漏

Android 中的 WebView 存在很大的兼容性問題,有些 WebView 甚至存在內存泄露的問題。所以通常根治這個問題的辦法是為 WebView 開啟另外一個進程,通過 AIDL 與主進程進行通信, WebView 所在的進程可以根據業務的需要選擇合適的時機進行銷毀,從而達到內存的完整釋放。

AlertDialog 造成的內存泄露

new AlertDialog.Builder(this)
        .setPositiveButton("Baguette", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                MainActivity.this.makeBread();
            }
        }).show();

DialogInterface.OnClickListener 的匿名實現類持有了 MainActivity 的引用;

而在 AlertDialog 的實現中,OnClickListener 類將被包裝在一個 Message 對象中(具體可以看 AlertController 類的 setButton 方法),而且這個 Message 會在其內部被復制一份(AlertController 類的 mButtonHandler 中可以看到),兩份 Message 中只有一個被 recycle,另一個(OnClickListener 的成員變量引用的 Message 對象)將會泄露!

解決辦法:

  • Android 5.0 以上不存在此問題;

  • Message 對象的泄漏無法避免,但是如果僅僅是一個空的 Message 對象,將被放入對象池作為后用,是沒有問題的;

  • 讓 DialogInterface.OnClickListener 對象不持有外部類的強引用,如用 static 類實現;

  • 在 Activity 退出前 dismiss dialog

Drawable 引起的內存泄露

Android 在 4.0 以后已經解決了這個問題。這里可以跳過。

當我們屏幕旋轉時,默認會銷毀掉當前的 Activity,然后創建一個新的 Activity 并保持之前的狀態。在這個過程中,Android 系統會重新加載程序的UI視圖和資源。假設我們有一個程序用到了一個很大的 Bitmap 圖像,我們不想每次屏幕旋轉時都重新加載這個 Bitmap 對象,最簡單的辦法就是將這個 Bitmap 對象使用 static 修飾。

private static Drawable sBackground;

@Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad");

if (sBackground == null) {
    sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);

setContentView(label);

}</code></pre>

但是上面的方法在屏幕旋轉時有可能引起內存泄露,因為,當一個 Drawable 綁定到了 View 上,實際上這個 View 對象就會成為這個 Drawable 的一個 callback 成員變量,上面的例子中靜態的 sBackground 持有 TextView 對象的引用,而 TextView 持有 Activity 的引用。當屏幕旋轉時,Activity 無法被銷毀,這樣就產生了內存泄露問題。

該問題主要產生在 4.0 以前,因為在 2.3.7 及以下版本 Drawable 的 setCallback 方法的實現是直接賦值,而從 4.0.1 開始,setCallback 采用了弱引用處理這個問題,避免了內存泄露問題。

資源未關閉造成的內存泄露

  • BroadcastReceiver,ContentObserver 之類的沒有解除注冊

  • Cursor,Stream 之類的沒有 close

  • 無限循環的動畫在 Activity 退出前沒有停止

  • 一些其他的該 release 的沒有 release,該 recycle 的沒有 recycle… 等等。

總結

我們不難發現,大多數問題都是 static 造成的!

  • 在使用 static 時一定要小心,關注該 static 變量持有的引用情況。在必要情況下使用弱引用的方式來持有一些引用

  • 在使用非靜態內部類時也要注意,畢竟它們持有外部類的引用。(使用 RxJava 的同學在 subscribe 時也要注意 unSubscribe)

  • 注意在生命周期結束時釋放資源

  • 使用屬性動畫時,不用的時候請停止(尤其是循環播放的動畫),不然會產生內存泄露(Activity 無法釋放)(View 動畫不會)

幾種內存檢測工具的介紹

  • Memory Monitor

  • Allocation Tracker

  • Heap Viewer

  • LeakCanary

Memory Monitor

位于 Android Monitor 中,該工具可以:

  • 方便的顯示內存使用和 GC 情況

  • 快速定位卡頓是否和 GC 有關

  • 快速定位 Crash 是否和內存占用過高有關

  • 快速定位潛在的內存泄露問題(內存占用一直在增長)

  • 但是不能準確的定位問題

Allocation Tracker

該工具用途:

  • 可以定位代碼中分配的對象類型、大小、時間、線程、堆棧等信息

  • 可以定位內存抖動問題

  • 配合 Heap Viewer 定位內存泄露問題(可以找出來泄露的對象是在哪創建的等等)

使用方法:在 Memory Monitor 中有個 Start Allocation Tracking 按鈕即可開始跟蹤 在點擊停止跟蹤后會顯示統計結果。

Heap Viewer

該工具用于:

  • 顯示內存快照信息

  • 每次 GC 后收集一次信息

  • 查找內存泄露的利器

使用方法: 在 Memory Monitor 中有個 Dump Java Heap 按鈕,點擊即可,在統計報告左上角選按 package 分類。配合 Memory Monitor 的 initiate GC(執行 GC)按鈕,可檢測內存泄露等情況。

LeakCanary

重要的事情說三遍:

for (int i = 0; i < 3; i++) {
            Log.e(TAG, "檢測內存泄露的神器!");
        }

 

來自:https://segmentfault.com/a/1190000006852540

 

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