Android内存泄漏分析

概述

内存泄漏,即Memory Leak,指程序中不再使用到的对象因某种原因而无法被GC正常回收。发生内存泄漏,会导致一些不再使用到的对象没有及时释放,这些对象占据着宝贵的内存空间,很容易导致后续分配内存的时候,内存空间不足而出现OOM(内存溢出)。无用对象占据的空间越多,那么可用的空闲空间也就越少,GC就会更容易被触发,GC进行时会停止其他线程的工作,因此有可能造成卡顿等情况。

Java内存分配策略

Java程序运行时的内存分配策略有三种,分别是静态分配栈式分配堆分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)栈区堆区

  • 静态存储区(方法区):主要存放静态数据全局static数据常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
  • 栈区:当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。
  • 堆区:又称动态内存分配,通常就是指在程序运行时直接new出来的内存。这部分内存在不使用时将会由Java垃圾回收器来负责回收。
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保存于栈中,而其指向的对象实体存放在堆上,包括这个对象的所有成员变量s1和mSample1。

Java是如何管理内存

Java的内存管理就是对象的分配和释放问题。在Java中,通过关键字new为每个对象申请内存空间,所有的对象都在堆(Heap)中分配空间,对象的释放是由GC决定和执行的。
GC(Garbage Collection) 即垃圾回收机制,在Java虚拟机上运行的一个程序,它会监控对象的使用,将不再使用的对象释放,回收内存。

Java判断对象是否可以回收使用的是可达性分析算法。

可达性分析算法:通过一系列被称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从GC Roots到这个对象是不可达),则证明此对象是不可用的,所以它们会被判断为可回收对象。(如下图黑色的圆圈)

在Java语言中,可以作为GC Roots的对象有如下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(Native方法)引用的对象。
image

Java中的引用

在Java中,将引用方式分为:强引用软引用弱引用虚引用,这四种引用强度依次逐渐减弱。

强引用:类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用:用来描述一些还有用但并非必须的对象。在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。

弱引用:用户描述非必须对象的。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用:一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时刻得到一个系统通知。

内存泄漏的场景

静态变量内存泄漏

静态变量的生命周期跟整个程序的生命周期一致。只要静态变量没有被销毁也没有置为null,其对象就一直被保持引用,也就不会被垃圾回收,从而出现内存泄漏。

// 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作为静态变量,并且持有Activity的引用,sTest的生命周期肯定比Activity的生命周期长。因此当Activity退出后,由于Activity仍被sTest引用到,所以Activity就不能被回收,造成了内存泄漏。

Activity这种占用内存非常多的对象,内存泄漏的话影响非常大。

解决方案

  • 针对静态变量

在不使用静态变量时置为空,如:

sTest = null; 
  • 针对Context

如果用到Context,尽量去使用Application的Context,避免直接传递Activity,如:

sTest = new Test(getApplicationContext());
  • 针对Activity

若一定要使用Activity,建议使用弱引用或软引用来代替强引用。如:

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

单例内存泄漏

单例模式其生命周期跟应用一样,所以使用单例模式时传入的参数需要注意一下,避免传入Activity等对象造成内存泄漏。

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

说明:当创建这个单例对象的使用,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要;

  • 如果传入的是Application的Context,因为Application的生命周期就是整个应用的生命周期,所以这将没有任何问题。
  • 如果传入的是Activity的Context,当这个Context所对应的Activity退出时,由于该Context的引用被单例所持有,其生命周期等于整个应用程序的生命周期,所以当前Activity退出时它的内存并不会被回收,这就造成泄漏了。

解决方案

使用和单例生命周期一样的对象。

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

非静态内部类(匿名类)内存泄漏

非静态内部类(匿名类)默认就持有外部类的引用,当非静态内部类(匿名类)对象的生命周期比外部类对象的生命周期长时,就会导致内存泄漏。

Handler内存泄漏

如果Handler中有延迟任务或者等待执行的任务队列过长,都有可能因为Handler继续执行而导致Activity发生泄漏。

  1. 首先,非静态的Handler类会默认持有外部类的引用,如Activity等。

  2. 然后,还未处理完的消息(Message)中会持有Handler的引用。

  3. 还未处理完的消息会处于消息队列中,即消息队列MessageQueue会持有Message的引用。

  4. 消息队列MessageQueue位于Looper中,Looper的生命周期跟应用一致。

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

解决方法

  • 静态内部类+弱引用

静态内部类默认不持有外部类的引用,所以改成静态内部类即可。同时,可以采用弱引用来持有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);
}

多线程引起的内存泄漏

匿名Thread类里持有外部类的引用。当Activity退出时,Thread有可能还在后头执行,这时就会发生内存泄露。

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

    }
}).start();

解决方法

  • 静态内部类

静态内部类不持有外部类的引用。

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

  • Activity退出时,结束线程

这是让线程的生命周期跟Activity一致。

集合类内存泄漏

集合类添加元素后,将会持有元素对象的引用,导致该元素对象不能被垃圾回收,从而发生内存泄漏。

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

说明:虽然obj已经被置为空了,但是集合里还是持有Object的引用。

解决方法

  • 清空集合对象

java objectList.clear(); objectList = null;

未关闭资源对象内存泄漏

一些资源对象需要在不使用的时候主动去关闭或者注销掉,否则的话,他们不会被垃圾回收,从而造成内存泄漏。

注销监听器

当我们需要使用系统服务时,比如执行某些后台任务、为硬件访问提供接口等等系统服务。我们需要将自己注册到服务的监听器中,然而,这会让服务持有Activity的引用,如果忘记Activity销毁时取消注册,就会导致Activity泄露。

unregisterXxx(xxx);

关闭输入输出流

在使用IO、File流等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果不及时关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。

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

回收Bitmap

Bitmap对象比较占内存,当它不再被使用的时候,最好调用Bitmap.recycle()方法主动进行回收。

bitmap.recycle();
bitmap = null;

停止动画

属性动画中有一类无限动画,如果Activity退出时不停止动画的话,动画会一直执行下去。因为动画会持有View的引用,View又持有Activity,最终Activity就不能被回收掉。只要我们在Activity退出把动画停止掉即可。

animation.cancel();

销毁WebView

WebView在加载网页后会长期占用内存而不能被释放,因此在Activity销毁后要调用它的destory()方法来销毁它以释放内存。此外,WebView在Android 5.1上也会出现其他的内存泄露。

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

        mWebView.stopLoading();
        // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
        mWebView.getSettings().setJavaScriptEnabled(false);
        mWebView.clearHistory();
        mWebView.clearView();
        mWebView.removeAllViews();
        mWebView.destroy();

    }
    super.onDestroy();
}

内存分析工具

dumpsys

dumpsys命令可以查看内存使用情况。

adb shell dumpsys meminfo <packageName>
image

说明:可以通过页面关闭前后ViewsActivities的数量来判断是否发生泄漏。

Memory Profiler

Memory Profiler是Android Studio提供的一个内存分析工具。(本文使用的是Android Studio 3.3.1)

Memory Profiler面板介绍

image
  1. 用于强制执行垃圾回收Event的按钮。
  2. 用户捕获堆转储的按钮。
  3. 用于记录内存分配情况的按钮。
  4. 用于放大/缩小时间线的按钮。
  5. 用于跳转至实时内存数据的按钮。
  6. Event时间线,其显示Activity状态、用户输入Event和屏幕旋转Event。
  7. 内存使用量时间线,其包含以下内容:
  8. 一个显示每个内存类别使用多少内存的堆叠图表,如左侧的y轴以及顶部的彩色健所示。
  9. 虚线表示分配的对象数,如右侧的y轴所示。
  10. 用于表示每个垃圾回收Event的图标。

Dump Java Heap

这个功能是用来获取当前应用的内存快照。通过分析内存快照,查看指定类的实例在内存中的情况,及其对象的引用关系,来判断内存是否泄漏。

NOTE: 在dump前,先点击一下GC按钮来强制内存回收一下,这样分析内存比较准确。

123.png

MAT

MAT (Memory Analyzer Tool)是一个快速且功能丰富的Java堆分析器,可以帮助您查找内存泄漏并减少内存消耗。
MAT下载地址:https://www.eclipse.org/mat/

Step1. 从AS的Memory Profiler中导出.hprof内存快照文件。

image

Step2. 转换.hprof文件。

AS导出的.hprof文件只能在AS的Memory Profiler中查看,要在MAT中查看,要使用hprof-conv进行转换。
hprof-conv工具的路径:<android_sdk>/paltform-tools/

转换命令:

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

Step3. 在MAT中打开转换好的.hprof文件。

image

Histogram

Histogram是从类的角度进行分析,注重量的分析。

image

内存分析

Step1. 查询指定的类。

image

Step2. 查询指定的对象被引用的地方。

image
image

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

image
image

说明:从上图可以看到MainActivity被sTest对象的context属性强引用,导致MainActivity泄漏。

Dominator Tree

Dominator Tree是从对象实例的角度进行分析,注重引用关系分析。

image

内存分析:

Step1. 查询指定的类。

image

Step2. 选中指定的类实例进行分析。

image
image

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

image
image

说明:与通过Histogram分析得到的结论一样。

LeakCanary

LeakCanary是Square开源的Android和Java的内存泄漏检测库。
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...
    }
}

使用

内存泄漏代码:

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

运行应用,并退出首页,LeakCanary就会检测到MainActivity泄漏。

image

说明:从LeakCanary的检测结果可以看出,是因为MainActivity中的sTest对象的context属性持有MainActivity而导致其泄漏。

转载地址:https://zhuanlan.zhihu.com/p/56961372

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容

  • 我的技术博客:移动开发小水吧 什么是内存泄漏 只要是现代智能电子设备,不管或大或小,都会有一个叫做内存的硬件,在手...
    贾亦真亦贾阅读 629评论 0 0
  • 内存泄漏产生的原因Android系统会为应用运行分配内存,当分配的内存不足时就会触发GC,GC采用的垃圾标记算法是...
    angeliur阅读 244评论 0 1
  • 【Android 内存泄漏】 引用: ★★★ 【知识必备】内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟...
    Rtia阅读 739评论 0 2
  • 如果一个无用的对象(不需要在使用的对象),仍然被其他对象持有使用,造成该对象无法被系统回收。以致该对象在堆中对占用...
    S_ZY阅读 379评论 0 4
  • 2018年过去了大半年时间,这半年的时间收获真的非常多! 参加了mj老师的《超级数字力》线下课; 加入了007社群...
    财富花园燕姐阅读 550评论 5 4