Android 內存泄露總結(附內存檢測工具)

Java 中的內存分配

主要是分三塊:git

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

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

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

棧與堆的區別

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

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

對於非靜態變量的儲存位置,咱們能夠粗暴的認爲:app

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

  • 成員變量位於堆中。由於它們屬於類,該類最終被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
to briefly exceed its memory limits and crash. 原文

大體意思是:由於在 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);
}

可是上面的方法在屏幕旋轉時有可能引發內存泄露,由於,當一個 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, "檢測內存泄露的神器!");
        }

LeakCanary 具體使用再也不贅述,自行 Google。

相關文章
相關標籤/搜索