内存泄露:分析及工具使用

说干就干!

一、内存泄漏说明

要讲解 Android 的内存泄漏,要从Java的内存机制讲起。

1、Java 内存回收机制

Java 中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的。GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。在Java语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值null,以下再没有调用过,另一个是给对象赋予了新值,这样重新分配了内存空间。

2、内存泄漏 & 内存溢出

内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放(这些对象不会被GC所回收,然而它却占用内存),从而造成内存空间的浪费称为内存泄漏。内存泄露有时不严重且不易察觉,这样开发者就不知道存在内存泄露,一次内存泄露的危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,最终会导致内存溢出,内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory(OOM)。

3、Java 内存分配策略

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

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
  • 栈区 :当方法被执行时,方法体内的局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

4、Android 中常见的内存泄漏

这个网上有大把讲解得很好的文章:
Android内存泄漏总结
Java中的内存泄漏

二、Android Profiler

Android Profiler 是一个先进的分析工具,能够实时展示cpu数据更新、内存和网络活动。在 Android Studio 2.4 preview版本中,Android Profiler窗口代替了Android Monitor,这里采用 Android Studio 3.0 Canary版本来写Demo,都一样。

Android Profiler 的默认窗口如图,它为每个分析器显示了一个简化的数据集:

  • ① 选择设备
  • ② 选择想要分析的应用进程
  • ③ 时间轴缩放控件
  • ④ Live 是一个跳转查看实时数据的按钮
  • ⑤ 事件时间线
  • ⑥ 点击可分别进入CPU、内存和网络活动等的详细窗口

这里写图片描述

ps:这个 Advanced profiling 需要 Android Plugin Version 为 2.4 及以上才能配置,明明采用最新版本【Android Plugin Version】【Gradle version】,但总是报“too old”,相信大家都遇到过这个操蛋问题 …

这里写图片描述

三、Memory Profiler

本文主要讲解内存,所以单说 Memory Profiler(内存分析器):

在窗口的工具栏顶部有三个按钮:

  • ① Force garbage collection(GC)
    • 就是手动调用GC,我们在抓内存前,一定要手动触发GC,这样抓到的内存使用情况就是不包括 Unreachable 对象的
    • Unreachable 指的是可以被垃圾回收器回收的对象,但是由于没有GC发生,所以没有释放,这时抓的内存使用中的 Unreachable 就是这些对象
  • ② Dump Java heap(捕获当前.hprof文件)
  • ③ Record memory allocations(记录某段内存分配)

这里写图片描述

上面是捕获一次heap dump或者录制一段内存分配的结果显示:

  • ④显示了时间轴内录制事件,结果显示在时间轴下面的窗口⑤中。
  • 在⑤中点击选择一个类,在右边的窗口⑥中会出现这个类的所有实例。
  • 点击窗口⑥选中一个实例对象,下方会出现窗口⑦。
  • 窗口⑦显示的是内存分配的堆栈跟踪(顶部栏点击 Record memory allocations)或者显示的是剩余对该对象的引用(顶部栏点击 Dump Java heap)。

你还可以在跟踪内存分配的同时捕获一段 dump heap,这样可以查看 dump heap 中的堆栈跟踪:

ps:此例模拟单例模拟泄漏问题

public class Util {

    private static Util mInstance;
    private Context mContext;

    private Util(Context context) {
        this.mContext = context;
    }

    public static Util getInstance(Context context) {
        if (mInstance == null) {
            mInstance = new Util(context);
        }
        return mInstance;
    }
}
public class MatDemoActivity extends AppCompatActivity {

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

        Util util = Util.getInstance(this);
    }
}

这里写图片描述

当旋转手机屏幕时,就会造成内存泄漏,通过分析引用关系可以找到泄漏原因,是不是很简单。

ps:有时在操作某一功能 / 模块时有卡顿或者内存占用飙升,手动点击GC按钮后,内存占用没有恢复到之前得水平,就可对此功能 / 模块所在包中的类进行堆栈跟踪,分析是否内存溢出。

ps:通过点击 Arrange by package 以包的形式排列要分析的类:

这里写图片描述

四、Android Profiler 和 MemoryAnalyzerTool 的关系

运用 Android Profiler(Memory Profiler)确实是能进行内存泄漏分析,但要从一堆引用关系中查找问题,费时费力(还有运气成分:P),这时就得Mat登场了:Mat 是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。

相对于 Android Profiler(Memory Profiler)/ Android Monitor,Mat更为强大,功能更为丰富,能为程序员节约不少时间。

通过此按钮可以导出用于内存泄漏分析的.hprof文件(或者通过 tools/Android/Android Device Monitor 导出 .hprof 文件):

这里写图片描述

但是此 .hprof 文件必须转换成标准的 .hprof 文件才能用 Mat 进行分析(原 Android Monitor 有这个选项,Memory Profiler 没有,且相对Android Monitor,Memory Profiler 简化了许多,更需要 Mat 的辅助分析啦):

  • ① 运行cmd,cd 到 as 自带的转换工具 hprof-conv.exe 目录下: ** \sdk\platform-tools
  • ② 运行指令:hprof-conv inputname.hprof outname.hprof,生成所需 .hprof 文件

五、Memory Analyzer Tool(Mat)

下载、安装、导入等按下不表,专说 Mat 的分析流程。

1、Mat主要分析内容

这里写图片描述

  • Histogram: Lists number of instances per class
    – 直方图:列出每个类的实例数
    – 通过 Histogram 可以直观地看出内存中不同类型的 buffer 的数量和占用的内存大小

  • Dominator Tree: List the biggest objects and what they keep alive
    – 支配树:列出最大的对象和他们保存的东西
    – Dominator Tree 是把内存中的对象按照从大到小的顺序进行排序

2、分析 Histogram

主要通过分析 Histogram(Dominator Tree 的分析思路与 Histogram 相同),及通过截取某一动作前后的 .hprof 文件,进行 Histogram 的对比分析:

这里写图片描述

  • ① 通过直接输入 com.xq(要分析的组件名 / 包名) 过滤需要分析的组件
  • ② 通过 Objects 可得知当前类有多少个实例,从而推测其是否有可能内存溢出
  • ③ 通过点击 Retained Heap 可按当前类占用内存的多少进行排序,从而获取占用大内存的类

3、分析存在问题的类

通过 with incoming reference 获取该实例的被应用关系:

这里写图片描述

通过 exclude all phantom/weak/soft etc. reference 过滤掉各种软引用、弱引用等会在 GC 时被回收的情况(能GC掉就说明不会内存泄漏):

这里写图片描述

最终得知实例在什么地方被引用,分析引用关系中,双方的生命周期是否一致,看是否会造成内存溢出:

这里写图片描述

4、对比分析

通过截取某一动作前后的 .hprof 文件,进行 Histogram 的对比分析:

首先,将要对比的两个 .hprof 文件的 Histogram 通过 Add to Compare Basket 添加到对比篮:

这里写图片描述

然后,点击 Compare the results 按钮进行对比:

这里写图片描述

最后,观察分析是否有问题,如果有问题,再根据上面的方法进行引用关系分析:

这里写图片描述


内存泄漏的分析及解决要依赖于大量的经验和实操,这里通过一个较为简单的例子,介绍一下分析流程,后面如果有更为贴合实际开发中的问题,亦或是更为深入的内存泄漏分析,要多些思考,多做记录 …


参考文章:
https://github.com/GeniusVJR/LearningNotes/blob/master/Part2/JavaSE/Java%E4%B8%AD%E7%9A%84%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md
https://github.com/GeniusVJR/LearningNotes/blob/master/Part1/Android/Android%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E6%80%BB%E7%BB%93.md
http://blog.csdn.net/shenyan1990/article/details/70052670
http://blog.csdn.net/zxm317122667/article/details/52162764

猜你喜欢

转载自blog.csdn.net/zeqiao/article/details/77334522