Android內(nèi)存泄漏分析

概述

內(nèi)存泄漏,即Memory Leak,指程序中不再使用到的對象因某種原因而無法被GC正?;厥?。發(fā)生內(nèi)存泄漏,會導(dǎo)致一些不再使用到的對象沒有及時釋放,這些對象占據(jù)著寶貴的內(nèi)存空間,很容易導(dǎo)致后續(xù)分配內(nèi)存的時候,內(nèi)存空間不足而出現(xiàn)OOM(內(nèi)存溢出)。無用對象占據(jù)的空間越多,那么可用的空閑空間也就越少,GC就會更容易被觸發(fā),GC進(jìn)行時會停止其他線程的工作,因此有可能造成卡頓等情況。

Java內(nèi)存分配策略

Java程序運(yùn)行時的內(nèi)存分配策略有三種,分別是靜態(tài)分配、棧式分配堆分配,對應(yīng)的,三種存儲策略使用的內(nèi)存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))、棧區(qū)堆區(qū)。

  • 靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)常量。這塊內(nèi)存在程序編譯時就已經(jīng)分配好,并且在程序整個運(yùn)行期間都存在。
  • 棧區(qū):當(dāng)方法被執(zhí)行時,方法體內(nèi)的局部變量都在棧上創(chuàng)建,并在方法執(zhí)行結(jié)束時這些局部變量所持有的內(nèi)存將會自動被釋放。
  • 堆區(qū):又稱動態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時直接new出來的內(nèi)存。這部分內(nèi)存在不使用時將會由Java垃圾回收器來負(fù)責(zé)回收。
public class Sample {

    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample mSample3 = new Sample();

說明

  • 局部變量s2和引用變量mSample2都位于棧中,但是mSample2指向的對象是存在于堆上的;
  • mSample3保存于棧中,而其指向的對象實(shí)體存放在堆上,包括這個對象的所有成員變量s1和mSample1。

Java是如何管理內(nèi)存

Java的內(nèi)存管理就是對象的分配和釋放問題。在Java中,通過關(guān)鍵字new為每個對象申請內(nèi)存空間,所有的對象都在堆(Heap)中分配空間,對象的釋放是由GC決定和執(zhí)行的。
GC(Garbage Collection) 即垃圾回收機(jī)制,在Java虛擬機(jī)上運(yùn)行的一個程序,它會監(jiān)控對象的使用,將不再使用的對象釋放,回收內(nèi)存。

Java判斷對象是否可以回收使用的是可達(dá)性分析算法。

可達(dá)性分析算法:通過一系列被稱為“GC Roots”的對象作為起點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個對象到GC Roots沒有任何引用鏈相連時(就是從GC Roots到這個對象是不可達(dá)),則證明此對象是不可用的,所以它們會被判斷為可回收對象。(如下圖黑色的圓圈)

在Java語言中,可以作為GC Roots的對象有如下幾種:

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象;
  • 方法區(qū)中類靜態(tài)屬性引用的對象;
  • 方法區(qū)中常量引用的對象;
  • 本地方法棧中JNI(Native方法)引用的對象。
image

Java中的引用

在Java中,將引用方式分為:強(qiáng)引用、軟引用、弱引用虛引用,這四種引用強(qiáng)度依次逐漸減弱。

強(qiáng)引用:類似“Object obj = new Object()”這類的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會回收掉被引用的對象。

軟引用:用來描述一些還有用但并非必須的對象。在系統(tǒng)將要發(fā)生內(nèi)存溢出之前,將會把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收。

弱引用:用戶描述非必須對象的。被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。

虛引用:一個對象是否有虛引用存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實(shí)例。為一個對象設(shè)置虛引用的唯一目的就是能在這個對象被收集器回收時刻得到一個系統(tǒng)通知。

內(nèi)存泄漏的場景

靜態(tài)變量內(nèi)存泄漏

靜態(tài)變量的生命周期跟整個程序的生命周期一致。只要靜態(tài)變量沒有被銷毀也沒有置為null,其對象就一直被保持引用,也就不會被垃圾回收,從而出現(xiàn)內(nèi)存泄漏。

// MainActivity.java
public class MainActivity extends AppCompatActivity {

    private static Test sTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        sTest = new Test(this);
    }
}

// Test.java
public class Test {
    private Context context;

    public Test(Context context) {
        this.context = context;
    }
}

說明:sTest作為靜態(tài)變量,并且持有Activity的引用,sTest的生命周期肯定比Activity的生命周期長。因此當(dāng)Activity退出后,由于Activity仍被sTest引用到,所以Activity就不能被回收,造成了內(nèi)存泄漏。

Activity這種占用內(nèi)存非常多的對象,內(nèi)存泄漏的話影響非常大。

解決方案

  • 針對靜態(tài)變量

在不使用靜態(tài)變量時置為空,如:

sTest = null; 
  • 針對Context

如果用到Context,盡量去使用Application的Context,避免直接傳遞Activity,如:

sTest = new Test(getApplicationContext());
  • 針對Activity

若一定要使用Activity,建議使用弱引用或軟引用來代替強(qiáng)引用。如:

// 弱引用   
WeakReference wakReference = new WeakReference<>(this);   
Activity activity = weakReference.get();
// 軟引用   
SoftReference softReference = new SoftReference<>(this);   
Activity activity = softReference.get();  

單例內(nèi)存泄漏

單例模式其生命周期跟應(yīng)用一樣,所以使用單例模式時傳入的參數(shù)需要注意一下,避免傳入Activity等對象造成內(nèi)存泄漏。

public class AppManager {
    private static AppManager instance;
    private Context context;

    private AppManager(Context context) {
        this.context = context;
    }

    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

說明:當(dāng)創(chuàng)建這個單例對象的使用,由于需要傳入一個Context,所以這個Context的生命周期的長短至關(guān)重要;

  • 如果傳入的是Application的Context,因?yàn)锳pplication的生命周期就是整個應(yīng)用的生命周期,所以這將沒有任何問題。
  • 如果傳入的是Activity的Context,當(dāng)這個Context所對應(yīng)的Activity退出時,由于該Context的引用被單例所持有,其生命周期等于整個應(yīng)用程序的生命周期,所以當(dāng)前Activity退出時它的內(nèi)存并不會被回收,這就造成泄漏了。

解決方案

使用和單例生命周期一樣的對象。

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;
    }
}

非靜態(tài)內(nèi)部類(匿名類)內(nèi)存泄漏

非靜態(tài)內(nèi)部類(匿名類)默認(rèn)就持有外部類的引用,當(dāng)非靜態(tài)內(nèi)部類(匿名類)對象的生命周期比外部類對象的生命周期長時,就會導(dǎo)致內(nèi)存泄漏。

Handler內(nèi)存泄漏

如果Handler中有延遲任務(wù)或者等待執(zhí)行的任務(wù)隊(duì)列過長,都有可能因?yàn)镠andler繼續(xù)執(zhí)行而導(dǎo)致Activity發(fā)生泄漏。

  1. 首先,非靜態(tài)的Handler類會默認(rèn)持有外部類的引用,如Activity等。

  2. 然后,還未處理完的消息(Message)中會持有Handler的引用。

  3. 還未處理完的消息會處于消息隊(duì)列中,即消息隊(duì)列MessageQueue會持有Message的引用。

  4. 消息隊(duì)列MessageQueue位于Looper中,Looper的生命周期跟應(yīng)用一致。

引用鏈:Looper -> MessageQueue -> Message -> Handler -> Activity

解決方法

  • 靜態(tài)內(nèi)部類+弱引用

靜態(tài)內(nèi)部類默認(rèn)不持有外部類的引用,所以改成靜態(tài)內(nèi)部類即可。同時,可以采用弱引用來持有Activity的引用。(也可以使用WeakHandler庫:https://github.com/badoo/android-weak-handler)

private static class MyHandler extends Handler {
  private WeakReference<Activity> mWeakReference;

  public MyHandler(Activity activity) {
      mWeakReference = new WeakReference<>(activity);
  }

  @Override
  public void handleMessage(Message msg) {
      super.handleMessage(msg);
      //...
  }
}
  • Activity退出時,移除所有信息

移除信息后,Handler將會跟Activity生命周期同步。

@Override   
protected void onDestroy() {       
   super.onDestroy();
   mHandler.removeCallbacksAndMessages(null);
}

多線程引起的內(nèi)存泄漏

匿名Thread類里持有外部類的引用。當(dāng)Activity退出時,Thread有可能還在后頭執(zhí)行,這時就會發(fā)生內(nèi)存泄露。

new Thread(new Runnable() {
    @Override
    public void run() {

    }
}).start();

解決方法

  • 靜態(tài)內(nèi)部類

靜態(tài)內(nèi)部類不持有外部類的引用。

private static class MyThread extends Thread { // ... }

  • Activity退出時,結(jié)束線程

這是讓線程的生命周期跟Activity一致。

集合類內(nèi)存泄漏

集合類添加元素后,將會持有元素對象的引用,導(dǎo)致該元素對象不能被垃圾回收,從而發(fā)生內(nèi)存泄漏。

List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    Object obj = new Object();
    objectList.add(obj);
    obj = null;
}

說明:雖然obj已經(jīng)被置為空了,但是集合里還是持有Object的引用。

解決方法

  • 清空集合對象

java objectList.clear(); objectList = null;

未關(guān)閉資源對象內(nèi)存泄漏

一些資源對象需要在不使用的時候主動去關(guān)閉或者注銷掉,否則的話,他們不會被垃圾回收,從而造成內(nèi)存泄漏。

注銷監(jiān)聽器

當(dāng)我們需要使用系統(tǒng)服務(wù)時,比如執(zhí)行某些后臺任務(wù)、為硬件訪問提供接口等等系統(tǒng)服務(wù)。我們需要將自己注冊到服務(wù)的監(jiān)聽器中,然而,這會讓服務(wù)持有Activity的引用,如果忘記Activity銷毀時取消注冊,就會導(dǎo)致Activity泄露。

unregisterXxx(xxx);

關(guān)閉輸入輸出流

在使用IO、File流等資源時要及時關(guān)閉。這些資源在進(jìn)行讀寫操作時通常都使用了緩沖,如果不及時關(guān)閉,這些緩沖對象就會一直被占用而得不到釋放,以致發(fā)生內(nèi)存泄露。

inputStream.close();
outputStream.close();

回收Bitmap

Bitmap對象比較占內(nèi)存,當(dāng)它不再被使用的時候,最好調(diào)用Bitmap.recycle()方法主動進(jìn)行回收。

bitmap.recycle();
bitmap = null;

停止動畫

屬性動畫中有一類無限動畫,如果Activity退出時不停止動畫的話,動畫會一直執(zhí)行下去。因?yàn)閯赢嫊钟蠽iew的引用,View又持有Activity,最終Activity就不能被回收掉。只要我們在Activity退出把動畫停止掉即可。

animation.cancel();

銷毀WebView

WebView在加載網(wǎng)頁后會長期占用內(nèi)存而不能被釋放,因此在Activity銷毀后要調(diào)用它的destory()方法來銷毀它以釋放內(nèi)存。此外,WebView在Android 5.1上也會出現(xiàn)其他的內(nèi)存泄露。

@Override
protected void onDestroy() {
    if (mWebView != null) {
        ViewParent parent = mWebView.getParent();
        if (parent != null) {
            ((ViewGroup) parent).removeView(mWebView);
        }

        mWebView.stopLoading();
        // 退出時調(diào)用此方法,移除綁定的服務(wù),否則某些特定系統(tǒng)會報錯
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.clearView();
        mWebView.removeAllViews();
        mWebView.destroy();

    }
    super.onDestroy();
}

內(nèi)存分析工具

dumpsys

dumpsys命令可以查看內(nèi)存使用情況。

adb shell dumpsys meminfo <packageName>
image

說明:可以通過頁面關(guān)閉前后ViewsActivities的數(shù)量來判斷是否發(fā)生泄漏。

Memory Profiler

Memory Profiler是Android Studio提供的一個內(nèi)存分析工具。(本文使用的是Android Studio 3.3.1)

Memory Profiler面板介紹

image
  1. 用于強(qiáng)制執(zhí)行垃圾回收Event的按鈕。
  2. 用戶捕獲堆轉(zhuǎn)儲的按鈕。
  3. 用于記錄內(nèi)存分配情況的按鈕。
  4. 用于放大/縮小時間線的按鈕。
  5. 用于跳轉(zhuǎn)至實(shí)時內(nèi)存數(shù)據(jù)的按鈕。
  6. Event時間線,其顯示Activity狀態(tài)、用戶輸入Event和屏幕旋轉(zhuǎn)Event。
  7. 內(nèi)存使用量時間線,其包含以下內(nèi)容:
  8. 一個顯示每個內(nèi)存類別使用多少內(nèi)存的堆疊圖表,如左側(cè)的y軸以及頂部的彩色健所示。
  9. 虛線表示分配的對象數(shù),如右側(cè)的y軸所示。
  10. 用于表示每個垃圾回收Event的圖標(biāo)。

Dump Java Heap

這個功能是用來獲取當(dāng)前應(yīng)用的內(nèi)存快照。通過分析內(nèi)存快照,查看指定類的實(shí)例在內(nèi)存中的情況,及其對象的引用關(guān)系,來判斷內(nèi)存是否泄漏。

NOTE: 在dump前,先點(diǎn)擊一下GC按鈕來強(qiáng)制內(nèi)存回收一下,這樣分析內(nèi)存比較準(zhǔn)確。

123.png

MAT

MAT (Memory Analyzer Tool)是一個快速且功能豐富的Java堆分析器,可以幫助您查找內(nèi)存泄漏并減少內(nèi)存消耗。
MAT下載地址:https://www.eclipse.org/mat/

Step1. 從AS的Memory Profiler中導(dǎo)出.hprof內(nèi)存快照文件。

image

Step2. 轉(zhuǎn)換.hprof文件。

AS導(dǎo)出的.hprof文件只能在AS的Memory Profiler中查看,要在MAT中查看,要使用hprof-conv進(jìn)行轉(zhuǎn)換。
hprof-conv工具的路徑:<android_sdk>/paltform-tools/

轉(zhuǎn)換命令:

hprof-conv heap-original.hprof heap-converted.hprof

Step3. 在MAT中打開轉(zhuǎn)換好的.hprof文件。

image

Histogram

Histogram是從類的角度進(jìn)行分析,注重量的分析。

image

內(nèi)存分析

Step1. 查詢指定的類。

image

Step2. 查詢指定的對象被引用的地方。

image
image

Step3. 合并到GC Roots的最短路徑。

image
image

說明:從上圖可以看到MainActivity被sTest對象的context屬性強(qiáng)引用,導(dǎo)致MainActivity泄漏。

Dominator Tree

Dominator Tree是從對象實(shí)例的角度進(jìn)行分析,注重引用關(guān)系分析。

image

內(nèi)存分析:

Step1. 查詢指定的類。

image

Step2. 選中指定的類實(shí)例進(jìn)行分析。

image
image

Step3. 合并到GC Roots的最短路徑。

image
image

說明:與通過Histogram分析得到的結(jié)論一樣。

LeakCanary

LeakCanary是Square開源的Android和Java的內(nèi)存泄漏檢測庫。
LeakCanary地址:https://github.com/square/leakcanary

集成LeakCanary

build.gradle中配置:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
// Optional, if you use support library fragments:
debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3'

Application類中配置:

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
        // Normal app init code...
    }
}

使用

內(nèi)存泄漏代碼:

// MainActivity.java
public class MainActivity extends AppCompatActivity {

    private static Test sTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        sTest = new Test(this);
    }
}

// Test.java
public class Test {
    private Context context;

    public Test(Context context) {
        this.context = context;
    }
}

運(yùn)行應(yīng)用,并退出首頁,LeakCanary就會檢測到MainActivity泄漏。

image

說明:從LeakCanary的檢測結(jié)果可以看出,是因?yàn)镸ainActivity中的sTest對象的context屬性持有MainActivity而導(dǎo)致其泄漏。

轉(zhuǎn)載地址:https://zhuanlan.zhihu.com/p/56961372

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容