Android 性能优化之内存泄漏

在 Android 开发的过程中,经常需要注意内存泄漏问题,不然很容易导致 OOM 问题,或者因此引起频繁 gc 造成 app 卡顿。下面这篇文章将分析内存泄漏的原因、Android 内存管理的相关内容,并分享一些检测泄漏的方法和如何避免内存泄漏。

内存泄漏的定义

Android 是基于 Java 的,众所周知 Java 语言的内存管理是其一大特点,不用像 C 语言那样处理对象的内存分配到回收的全部过程。在 Java 中我们只需要简单地新建对象就可以了,Java 垃圾回收器会负责回收释放对象内存。这么看的话,垃圾回收器会管理内存又怎么还会发生内存泄漏呢?

其实 Java 中的内存泄漏的定义是:对象不再被程序所使用,但是由于这些对象被引用着导致 GC(Garbage Collector)不能回收它们。

下面这张图可以帮助我们更好地理解对象的状态,以及内存泄漏的情况

左边未引用的对象是会被 GC 回收的,右边被引用的对象不会被 GC 回收,但是未使用的对象中除了未引用的对象,还包括已被引用的一部分对象,那么内存泄漏久发生这部分已被引用但未使用的对象。

接下来还有一个疑问:未使用的对象被谁引用会让 GC 无法回收呢?

现在主流的程序语言的主流实现中,是通过可达性分析(Reachability Analysis)来判断对象是否存活的。这个算法的基本思路是:通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链时,说明此对象不可用,可以被回收了。

可以作为 GC Roots 的对象包括下面几种:

  • 虚拟机栈中引用的对象, 一般是当前在使用中局部变量

  • 方法区中类静态属性引用的对象, 就是静态变量对应的对象

  • 方法区中常量引用的对象

  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

MAT 分析内存泄漏的时候,也是查看对象到 GC Roots 的引用链,来定位泄漏代码的位置。

所以未使用的对象直接或间接地被 GC Roots 引用时会让 GC 无法回收,从而产生内存泄漏。这里说的引用都是强引用,Java 中还有三种引用类型:软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)。

  • 强引用是使用最普遍的引用,当强引用的对象到 GC Root 存在引用链,垃圾回收器绝不会回收它。

  • 软引用是内存空间不足时,GC 才会回收这些对象的内存,可用来实现内存敏感的高速缓存。

  • 弱引用是不管内存空间是否足够,下次 GC 都会回收它的内存,可用来解决内存泄漏问题。

  • 虚引用就是形同虚设,跟没有任何引用一样,主要用来跟踪对象被垃圾回收器回收的活动,虚引用必须和引用队列联合使用。

Android 的内存管理

了解了 Java 的内存泄漏的起因,接下来大致了解 Android 中的内存管理机制。

Google 在 Android 的官网上有这样一篇文章,初步介绍了 Android 是如何管理应用的进程与内存分配:http://developer.android.com/training/articles/memory.html。 Android 系统的 Dalvik 虚拟机扮演了常规的内存垃圾自动回收的角色,Android 系统没有为内存提供交换区,它使用 pagingmemory-mapping(mmapping) 的机制来管理内存,下面简要概述一些 Android 系统中重要的内存管理基础概念。

分配与回收内存

每一个进程的 Dalvik heap 都反映了使用内存的占用范围。这就是通常逻辑意义上提到的 Dalvik Heap Size,它可以随着需要进行增长,但是增长行为会有一个系统为它设定的上限。

逻辑上讲的 Heap Size 和实际物理意义上使用的内存大小是不对等的,Proportional Set Size (PSS) 记录了应用程序自身占用以及和其他进程进行共享的内存。

Android 系统并不会对 Heap 中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断 Heap 的尾端剩余空间是否足够,如果空间不够会触发 gc 操作,从而腾出更多空闲的内存空间。在 Android 的高级系统版本里面针对 Heap 空间有一个 Generational Heap Memory 的模型,最近分配的对象会存放在 Young Generation 区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到 Old Generation,最后累积一定时间再移动到 Permanent Generation 区域。系统会根据内存中不同的内存数据类型分别执行不同的 gc 操作。例如,刚分配到 Young Generation 区域的对象通常更容易被销毁回收,同时在 Young Generation 区域的 gc 操作速度会比 Old Generation 区域的 gc 操作速度更快。如下图所示:

每一个 Generation 的内存区域都有固定的大小,随着新的对象陆续被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发 GC 的操作,以便腾出空间来存放其他新的对象。如下图所示:

通常情况下,GC 发生的时候,所有的线程都是会被暂停的。执行 GC 所占用的时间和它发生在哪一个 Generation 也有关系,Young Generation 中的每次 GC 操作时间是最短的,Old Generation 其次,Permanent Generation 最长。执行时间的长短也和当前 Generation 中的对象数量有关,遍历树结构查找 20000 个对象比起遍历 50 个对象自然是要慢很多的。

为什么通常情况下,GC 发生的时候,所有的线程都会被暂停?

因为每次 GC 的时候,需要先找到可作为 GC Roots 的对象,然后以此搜索引用链,这个过程需要在一致性的内存快照中进行。这个“一致性”表示在整个过程中不能出现对象引用关系不断变化的情况,所以需要暂停所有的执行线程。

限制应用的内存

为了整个 Android 系统的内存控制需要,Android 系统为每一个应用程序都设置了一个硬性的 Dalvik Heap Size 最大限制阈值,这个阈值在不同的设备上会因为 RAM 大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起 OutOfMemoryError 的错误。

ActivityManager.getMemoryClass() 可以用来查询当前应用的 Heap Size 阈值,这个方法会返回一个整数,表明你的应用的 Heap Size 阈值是多少 Mb(megabates)。

还有一个用 adb 命令查询的方法:

adb shell getprop dalvik.vm.heapgrowthlimit

Dalvik 虚拟机的内存分为 Dalvik Heap 和 Native Heap。Dalvik Heap 是用来分配 Java 对象的,Native Heap 是 Native Code(即 C/C++ 方法)分配的内存。在 Android 中计算是否 OOM 的时候还有一个 Bitmap Memory,也称为 External Memory,是存储 Bitmap 的 byte 数据的。

因为 Android 系统版本的差异,判断 OOM 的条件会有所不同,主要是因为 Bitmap Memory 的分配差异的原因:

  • Android 2.x 系统时,Bitmap Memory 是分配在 Native Heap 中的,dalvik allocated + bitmap allocated + 新分配的大小 >= getMemoryClass() 值就会发生 OOM。

  • Android 3.x 以上的系统,Bitmap Memory 改到在 Dalvik Heap 中申请,所以 dalvik allocated + 新分配的大小 >= getMemoryClass() 就会发生 OOM。

检测与定位内存泄漏

(1) adb 命令

可以用 adb 命令查看内存的详细信息

1
adb shell dumpsys meminfo {package_name}

退出应用后,过几秒(或者更长时间)运行命令如果 ViewsViewRootImpActivities 的数量不为空就表示有内存泄露。这时需要把把内存快照导出来

1
2
3
adb shell am dumpheap {process_name} /data/local/tmp/{package_name}.hprof
adb pull /data/local/tmp/{package_name}.hprof ./
hprof-conv {package_name}.hprof {package_name}-conv.hprof

{process_name}在没有使用android:process情况下默认为包名。

导出 hprof 文件后需要用 SDK 自带 platform-tools 目录下的 hprof-conv 工作转换以下才能用 MAT 分析。

(2) Android Studio 的 Memory Monitor

这部分内容请看 Android 开发官网文章:

Optimize Memory Use with Memory Monitor

HPROF Viewer and Analyzer

Memory Monitor 还可以分析一段时间内的内存分配信息,具体看 Allocation Tracker

(3) LeakCanary

LeakCanary 是 Square 公司的开源项目,自动分析内存泄露问题,更多使用说明请看LeakCanary中文使用说明

如果出现内存泄露,想要具体地分析 HPROF 文件,位置为/sdcard/Download/leakcanary-{package_name}/

(4) MAT

MAT,即 Memory Analyzer Tools,原来是 eclipse 下的一个插件,不过也可以下载独立运行的应用,下载地址为MAT Download

在分析 HPROF 文件之前需要用hprof-conv转换一下。

下面介绍几个概念:

shallow size 是对象本身占用内存的大小,不包含其引用的对象。

Retained Heap 表示如果一个对象释放后,因此其释放而减少引用进而被释放的所有对象(包括被递归释放的)所占用的 heap 大小。

outgoing references : 表示被该对象引用的对象

incoming references : 表示引用到该对象的对象

在 Android 检查内存泄漏,主要搜索 Activity、Fragment、View 有没有泄漏。

查看泄漏对象的引用链:右键对象 -> with incoming references -> Path to GC roots exclude weak/soft reference

如何避免内存的总结

  1. 注意 Activity 的泄漏
  • 内部类引用导致 Activity 泄漏

具体见 Android 中由 Handler 和内部类引起的内存泄

  • Activity Context 被间接引用

对于大部分非必须使用 Activity Context 的情况(Dialog 的 Context 就必须是 Activity Context),我们都可以考虑使用 Application Context 而不是 Activity 的 Context,这样可以避免不经意的 Activity 泄露

  1. 注意静态变量和单例模式

静态变量是作为 GC Roots,在 Android 其生命周期基本和进程一样长,所以要非常静态变量引用其他生命周期的对象。虽然单例模式简单实用,提供了很多便利性,但是因为单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。

  1. 注意容器中对象泄漏

有时候,我们为了提高对象的复用性把某些对象放到缓存容器中,可是如果这些对象没有及时从容器中清除,也是有可能导致内存泄漏的。例如,针对 2.3 的系统,如果把 drawable 添加到缓存容器,因为 drawable 与 View 的强应用,很容易导致 activity 发生泄漏。而从 4.0开始,就不存在这个问题。解决这个问题,需要对 2.3 系统上的缓存 drawable 做特殊封装,处理引用解绑的问题,避免泄漏的情况。

  1. 注意监听器的注销

在 Android 程序里面存在很多需要 register 与 unregister 的监听器,我们需要确保在合适的时候及时 unregister 那些监听器。自己手动 add 的 listener,需要记得及时 remove 这个 listener。

  1. 注意 service 还在运行导致的泄露

  2. 及时关闭 Cursor

在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用 Cursor 之后没有及时关闭的情况。这些 Cursor 的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对 Cursor 对象的及时关闭。

参考文章:

END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!