剑客
关注科技互联网

Android性能优化之内存泄漏

欢迎转载,但请务必在明确位置注明文章出处! http://johnnyshieh.github.io/android/2016/11/18/android-memory-leak/

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

1. 内存泄漏的定义

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

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

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

Android性能优化之内存泄漏

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

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

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

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

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

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

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

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

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

所以未使用的对象直接或间接地被GC Roots引用时会让GC无法回收,从而产生内存泄漏。

2. 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操作速度更快。如下图所示:

Android性能优化之内存泄漏 Android性能优化之内存泄漏

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

Android性能优化之内存泄漏

通常情况下,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

3. 检测与定位内存泄漏

(1)adb命令

adb shell dumpsys meminfo {package_name}

(2)Android Studio的Memory Monitor

(3)LeakCanary

(4)MAT

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

4. 如何避免内存的总结

(1) 注意Activity的泄漏

  • 内部类引用导致Activity泄漏

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

  • Activity Context被间接引用

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

(2) 注意静态变量和单例模式

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

(3) 注意容器中对象泄漏

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

(4) 注意监听器的注销

(5) …

(6) 及时关闭Cursor

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

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址