详解Android内存泄漏检测与MAT使用

内存泄漏基本概念

内存检测这部分,相关的知识有JVM虚拟机垃圾收集机制,类加载机制,内存模型等。编写没有内存泄漏的程序,对提高程序稳定性,提高用户体验具有重要的意义。因此,学习Java利用java编写程序的时候,要特别注意内存泄漏相关的问题。虽然JVM提供了自动垃圾回收机制,但是还是有很多情况会导致内存泄漏。

内存泄漏主要原因就是一个生命周期长的对象,持有了一个生命周期短的对象的引用。这样,会导致短的对象在该回收时候无法被回收。Android中比较典型的有:1、静态变量持有Activity的context。2、或者Handler持有某个组件的context,同时如果Looper的消息队列中有针对该Handler的消息没有被处理,那么会被作为target持有强引用,最终的导致context无法释放,导致相应组件在退出时无法被内存回收。3、非静态内部类默认持有外部类的引用,这样如果我们在Activity中定义了一个Thread内部类,同时直接通过new Thread的方式去运行线程,那么在线程运行结束之前,线程都会持有Activity的引用,从而导致Activity无法被释放。

内存检测工具

LeakCananry

LeakCanary,主要监测的是使用过程中Activity,Fragment等组件是否没被内存回收。使用方法也十分简单,相当于装了一个监听器,然后通过正常 操作去寻找内存泄漏,发生内存泄漏的时候会有Toast,同时可以在相应程序查看哪里发生内存泄漏。
方法比较简单,添加leakcanary依赖以后,新建一个Application入口,在Oncreate方法中安装Leakcanary即可。

当发生内存泄漏时,屏幕会出现Toast,同时打开桌面上的Leaks程序,显示泄漏的内存,如下图:

LeakCananry实现步骤大致是:

实现大致步骤是:

1、自动把activity加入到KeyedWeakReference

2、在background线程中,检查onDestroy后reference是否被清除,且没有触发gc

3、如果reference没有被清除,则dump heap到一个hprof文件并保存到app文件系统中

4、在一个单独进程中启动HeapAnalyzerService,HeapAnalyzer使用HAHA来分析heap dump。

5、HeapAnalyzer在heap dump中根据reference key找到KeyedWeakReference。

6、HeapAnalyzer计算出到GC Roots的最短强引用路径来判断是否存在泄露,然后build出造成这个泄露的引用链。

7、结果被传回来app进程的DisplayLeakService,并展示一个泄露的notification。

方法的有点是简单易行,但是只能检测Activity、Fragment是否发生内存泄漏。

观看整体内存使用情况

详情参见官方文档: https://developer.android.com/studio/profile/investigate-ram.html#ViewingAllocations

使用adb shell,进入手机adb,执行命令:

dumpsys meminfo <包名> [-参数]

可以查看应用不同部分内存分配情况。比如Java heap,Native heap等

输出是目前具体应用的内存分配,单位是kilobytes

因为程序涉及jni,经常会分配本地内存,所以会使用adb shell 的方式去查看native heap的分配情况。

结果如下:

分析各个参数:

Private Clean/Dirty RAM:

这部分内存是app的私有内存,当app销毁是操作系统可以回收到的内存。其中private dirty只能被你的进程使用,同时只能存在在内存当中,当内存不够,也不能通过分页技术存储到硬盘(操作系统相关知识),dalvik和native heap上的分配都是private dirty RAM。因为是dalvik heap和native heap共享的内存,所以命名dirty?

DDMS

使用流程

  • 启动eclipse后,切换到DDMS透视图,并确认Devices视图、Heap视图都是打开的;
  • 将手机通过USB链接至电脑,链接时需要确认手机是处于“USB调试”模式,而不是作为“MassStorage”;
  • 链接成功后,在DDMS的Devices视图中将会显示手机设备的序列号,以及设备中正在运行的部分进程信息;
  • 点击选中想要监测的进程,比如system_process进程;
  • 点击选中Devices视图界面中最上方一排图标中的“Update Heap”图标;
  • 点击Heap视图中的“Cause GC”按钮;
  • 此时在Heap视图中就会看到当前选中的进程的内存使用量的详细情况。

如何检测内存泄漏?

Heap视图中部有一个Type叫做dataobject,即数据对象,也就是我们的程序中实例化的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。

正常情况下Total Size值都会稳定在一个有限的范围内,也就是说没有造成对象不被垃圾回收的情况,所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平。如果代码中存在没有释放对象引用的情况,则dataobject的Total Size值在每次GC后不会有明显的回落,随着操作次数的增多Total Size的值会越来越大

通过DDMS方式,DataObject 的totalSize如果稳定在一个大概范围内,则可以确定没有发生内存泄漏。

MAT

然而,并不是所有的内存泄漏都十分明显,并且会最终导致OOM。有时候只有几个对象被泄漏,虽然影响不大,但是无疑浪费了内存。

要发现这种比较隐蔽的内存泄漏,我们需要使用MAT工具。

在了解支配树之前,要先了解一些相关概念。

支配树

支配树体现了对象实例间的支配关系,在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。

在这张图里,左边是对象引用关系,对于A和B,要抵达这两个点必须经过GC root。而对于C可以从A也可以从B抵达,但都必须经过GC root,所以最近的支配点同样也是GC root。

对于点D,不管是从C->D还是C->D->F->D,都必须经过的最近的点是C,所以C是D的支配点。同理可得EFHG在支配树中的位置。

SHALLOWHEAP和RETAINED HEAP

Shallow heap表示对象本身所占内存大小,一个内存大小100bytes的对象Shallow heap就是100bytes。

Retained heap表示通过回收这一个对象总共能回收的内存,比方说一个100bytes的对象还直接或者间接地持有了另外3个100bytes的对象引用,回收这个对象的时候如果另外3个对象没有其他引用也能被回收掉的时候,Retained heap就是400bytes。

在使用mat进行分析时,我们常常接触到的数据就是shallow size和retained size: Shallow Size

对象自身占用的内存大小,不包括它引用的对象。

针对非数组类型的对象,它的大小就是对象与它所有的成员变量大小的总和。当然这里面还会包括一些java语言特性的数据存储单元。

针对数组类型的对象,它的大小是数组元素对象的大小总和。

Retained Size

Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C, C就是间接引用)
换句话说,Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。
不过,释放的时候还要排除被GC Roots直接或间接引用的对象。他们暂时不会被回收。如下图:

A对象的Retained Size=A对象的Shallow Size

B对象的Retained Size=B对象的Shallow Size + C对象的Shallow Size

因为B对象被释放时,C同时被释放,而D由于被GC roots直接引用所以不会被释放。而Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存。

以上概念,都是在使用MAT进行内存分析经常使用的,所以要记住。

MAT的下载与使用

下载地址:https://eclipse.org/mat/downloads.php

这里没有作为eclipse插件的方式下载mat,而是通过下载单独的软件客户端。

首先,在DDMS中选择要检测的进程并dump HPROF file,如下图:

HPROF中存储的是当前内存的快照,因此,在dump快照之前先点击cause GC手动触发一次垃圾回收,这样可以避免软引用、弱引用等不必要的对象保留在内存中影响我们的分析。

转储出来的hprof文件,还有使用sdk自带工具进行一下格式转化,工具在sdk路径下的platform-tools下,名称为hprof-conv。

使用方法:

/.hprof-conv.exe a.hprof b.hprof

a 是输入hprof文件名,b是输出文件名。

然后将b.hprof在eclipse memory Analyzer中打开,注意要转换格式,不然无法成功打开。

如下:

利用MAT分析内存泄漏

分析过程中,主要使用的是Histogram直方图,和Dominater tree支配树。

在Histogram视图中查找retained heap值最大的项,并分析这里是否发生内存泄漏。

注意,一般情况下我们忽略java、android系统自带的对象,而着重分析我们自己程序中的对象。所以在上面输入过滤Class Name。

Retained heap表示因为这个对象,会导致多少对象无法回收。

右击相应类,list objects->with incoming references。表明引用这个类的某个实例的其它类,也就是它在引用树中的父节点。通过分析该对象被谁引用,来判断为何没被垃圾回收。
outcoming reference就是子节点,查看一些当前对象引用着的对象。

此外看,Merge shortest path to gc root,可以找到一条到GC root的最短路径,来看为什么当前对象无法被回收。

实战分析

下面记录了本人对一个项目的具体分析过程,以及各个工具的使用方法。

1、使用DDMS查看内存

使用DDMS的过程中,针对应用分别进行了多次检测,主要查看程序运行前的内存使用情况和程序运行后的内存使用情况:

使用前:

使用后:

通过上述数据可以看到,在程序运行前data object也就是在堆上分配的数据是180KB左右,而运行后内存大概在300KB上下浮动,没有呈现一个明显的一直上升的情况,故而没有明显的内存泄漏,基本没有导致OOM的可能。

但是,可以发现,程序运行一次以后,放置一段时间,即便手动触发GC,堆上的内存虽然回落,但是仍然是288KB,与执行前的180KB相差较大,说明有一些对象被GC roots引用,无法完成释放。

下面采用MAT工具进行进一步分析。在上面的过程中,转出了三个hprof文件,将hprof文件利用Android sdk tools下的工具进行格式转换,进行对比分析:

2、使用MAT分析内存转储

前面分析内存使用发现,使用前和使用后有一个100KB左右的差值,同时即便放置一段时间仍然无法使用。将before和after的直方图加入对比栏,在MAT中进行对比:

点击右上角的红色叹号:

对比发现两个shallow heap大小基本相同,多出的部分是UpdatePartResultThread,系统类而不是我们自己编写程序造成的。
再看一下使用前后直方图中的retained heap:

可以看出,程序执行后,newActivity强引用了一些对象,在newAcitivity没有推出前,retainedheap部分内存无法被回收。这也就是我们在DDMS中发现堆内存差异的主要原因。

右击直方图中的NewActivity,可以看见如下选项:

用的比较多的是List objects和Merger shortest Paths to GC Roots。

List objects:

Outgoing reference是支配树中当前对象的子节点,也就是当前对象持有哪些引用。

Incoming reference是父节点,即当前对象被谁引用,为什么没被回收。

Merger shortest Paths to GC Roots:找到当前无法被释放的对象到GC roots的最短路径。即排查当前对象被谁引用,为什么没有被释放。这里因为我们的对象是一个Activity,当它显示在前台的时候,不会被垃圾回收,所以不是我们分析的点。

在这里,我们查看outgoing reference,查看当前对象拥有哪些强引用:

排除系统的对象,还是主要分析我们编写的程序。

最后发现,我们在之前使用LeakCanary时,注册的相应监听器没有回收,发现了内存泄漏 :)。

去掉LeakCanary,再次测试发现data object的值确实下降了不少。

继续分析,发现newActivity引用了一个

致使一部分内存无法被释放。这个问题属于客户端实现问题,不在内存泄漏的范围内。

接下来,在直方图中过滤出服务端的类:

可以看到,服务端的类大部分shallow heap都为0,也就是已经被垃圾回收。

结论

在使用MAT分析内存时,最关键的就是找引用关系。如果一个应该被释放的对象没有被释放,那么我们往往要查看它的incoming reference,看看是谁持有了它的强引用。同时利用Merger shortest GC roots找到到GC root的最短路径,确定是由于被谁引用而导致无法GC。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 5个Android开发中比较常见的内存泄漏问题及解决办法

    android中一个对象已经不需要了,但是其他对象还持有他的引用,导致他不能回收,导致这个对象暂存在内存中,这样内存泄漏就出现了. 内存泄漏出现多了,会是应用占用过多的没存,当占用的内存超过了系统分配的内存容量,就会出现内存溢出了导致应用Crash. 了解了内存泄漏的原因及影响后,我们需要做的就是掌握常见的内存泄漏,并在以后的Android程序开发中,尽量避免它.下面搜罗了5个Android开发中比较常见的内存泄漏问题及解决办法,分享给大家,一起来看看吧. 一.单例造成的内存泄漏 android

  • Android 内存泄漏的几种可能总结

    Java是垃圾回收语言的一种,其优点是开发者无需特意管理内存分配,降低了应用由于局部故障(segmentation fault)导致崩溃,同时防止未释放的内存把堆栈(heap)挤爆的可能,所以写出来的代码更为安全. 不幸的是,在Java中仍存在很多容易导致内存泄漏的逻辑可能(logical leak).如果不小心,你的Android应用很容易浪费掉未释放的内存,最终导致内存用光的错误抛出(out-of-memory,OOM). 一般内存泄漏(traditional memory leak)的原因

  • 详解Android性能优化之内存泄漏

    综述 内存泄漏(memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存.那么在Android中,当一个对象持有Activity的引用,如果该对象不能被系统回收,那么当这个Activity不再使用时,这个Activity也不会被系统回收,那这么以来便出现了内存泄漏的情况.在应用中内出现一次两次的内存泄漏获取不会出现什么影响,但是在应用长时间使用以后,若是存在大量的Activity无法被GC回收的话,最终会导致OOM的出现.那么我们在这就来分析一下导致内存泄漏的常见因素并且如何

  • Android内存泄漏实战解析

    Java是垃圾回收语言的一种,其优点是开发者无需特意管理内存分配,降低了应用由于局部故障(segmentation fault)导致崩溃,同时防止未释放的内存把堆栈(heap)挤爆的可能,所以写出来的代码更为安全. 不幸的是,在Java中仍存在很多容易导致内存泄漏的逻辑可能(logical leak).如果不小心,你的Android应用很容易浪费掉未释放的内存,最终导致内存用光的错误抛出(out-of-memory,OOM). 1.一般内存泄漏(traditional memory leak)的

  • Android 内存溢出和内存泄漏的问题

    Android 内存溢出和内存泄漏的问题 在面试中,经常有面试官会问"你知道什么是内存溢出?什么是内存泄漏?怎么避免?"通过这篇文章,你可以回答出来了. 内存溢出 (OOM)是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory:比如只申请了一个integer,但给它存了long才能存下的数,那就会出现内存溢出. 内存泄露 (memory leak)是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存

  • Android常见的几种内存泄漏小结

    一.背景 最近在项目的版本迭代中,出现了一些内存问题的小插曲,然后自己花了一些时间优化了APP运行时内存大小的问题,特此做个总结,与大家分享. 二.简介 在Android程序开发中,当一个对象已经不需要再使用了,本该被回收时,而另外一个正在使用的对象持有它的引用从而导致它不能被回收,这就导致本该被回收的对象不能被回收而停留在堆内存中,内存泄漏就产生了.内存泄漏有什么影响呢?它是造成应用程序OOM的主要原因之一.由于Android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时

  • Android 有效的解决内存泄漏的问题实例详解

    Android 有效的解决内存泄漏的问题 Android内存泄漏,我想做Android 应用的时候遇到的话很是头疼,这里是我在网上找的不错的资料,实例详解这个问题的解决方案 前言:最近在研究Handler的知识,其中涉及到一个问题,如何避免Handler带来的内存溢出问题.在网上找了很多资料,有很多都是互相抄的,没有实际的作用. 本文的内存泄漏检测工具是:LeakCanary  github地址:https://github.com/square/leakcanary 什么是内存泄漏? 内存泄漏

  • 分析Android内存泄漏的几种可能

    前言 内存泄漏简单地说就是申请了一块内存空间,使用完毕后没有释放掉.它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃.由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了. 从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在.真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存.从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内

  • 谈一谈Android内存泄漏问题

    内存泄漏:是指内存得不到GC的及时回收,从而造成内存占用过多,从而导致程序Crash,也就是常说的OOM. 一.static 先来看下面一段代码 public class DBHelper { private static DBHelper db= null; private DBHelper() { } public static DBHelper getInstance(Context context) { if (bitmapUtils == null) { synchronized (D

  • Android开发:浅谈MVP模式应用与内存泄漏问题解决

    最近博主开始在项目中实践MVP模式,却意外发现内存泄漏比较严重,但却很少人谈到这个问题,促使了本文的发布,本文假设读者已了解MVP架构. MVP简介 M-Modle,数据,逻辑操作层,数据获取,数据持久化保存.比如网络操作,数据库操作 V-View,界面展示层,Android中的具体体现为Activity,Fragment P-Presenter,中介者,连接Modle,View层,同时持有modle引用和view接口引用 示例代码 Modle层操作 public class TestModle

随机推荐