Android性能优化之内存泄漏

什么是内存泄漏?

部分对象的内存不在GC掌控之内。

垃圾回收机制

JAVA GC:某对象不再有任何的引用的时候才会被回收。

GCRoot

在JAVA中是通过可达性(Reachability Analysis)来判断对象是否存活,这个算法的基本思想是通过一系列的称谓”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走得路径称为引用链,当一个对象到GC Roots没有任何引用链相连则该对象被判定为可以被回收的对象,反之不能被回收

哪些对象可以作为GCRoot?

可以作为GCRoot引用点的是(GCRoot会去持有)

  • 激活状态的线程
  • 方法区中静态引用指向的对象
  • 方法区中常量引用指向的对象
  • Native方法中JNI引用的对象
  • Java栈中的引用对象,因为随时都可能被用到

怎么判断一个对象是垃圾对象?

这是一个主观的判断

假如new 了一个对象,进入一个activity,再退出,主动调用GC很多回,对象没有被释放。在GC眼里并不是垃圾对象,而在开发者眼里这个对象就是垃圾对象。

最坏的情况,App可能会因为大量的内存泄漏而导致内存耗尽,引发Crash,如果内存未耗尽,App也会犹豫内存空间不足,出现频繁的GC(垃圾回收),每次一出GC都是非常耗时的阻塞性操作,会造成设备非常严重的卡顿,给用户的体验就是,手机无论做什么操作,都是卡的,这也是Android设备玩久了之后常见的现象。

内存泄露多了容易导致OOM—-就像老司机开车开着开着就翻车了,内存溢出,app会崩溃

确定我们项目中是否存在内存泄露

粗略判断:

  • app用着用着变卡顿

  • 自动化测试中同一个操作内存一直升高没有释放

  • Android Studio monitor

    android studio有多个工具可以看到内存使用情况,具体可以这样操作:
    检查一个一个的动作。(比如activity的跳转),反复多次执行某一个操作,不断地通过这个工具查看内存的大概变化情况。若前后两个内存变化增加了不少则有可能发生了内存泄漏。

更仔细地查找内存泄漏的位置

  1. 使用Android Studio Heap SnapShot工具(堆栈快照)

分析:
Java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],对象本身的内存都很小。所以我们可以看到以Shallow Heap进行排序的Histogram图中,通常排在第一位第二位的是byte,char[]

名称 意义
Total Count 内存中该类的对象个数
Heap Count 堆内存中该类的对象个数
Sizeof 物理大小
Shallow size 该对象本身占有的内存大小
Retained size 释放该对象后,节省的内存大小

Retained Heap

比如: 一个ArrayList持有100,000个对象,每一个占用16 bytes,移除这些ArrayList可以释放16 x 100,000 + X,X代表ArrayList的shallow大小。相对于shallow heap,RetainedHeap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)。

Retained Heap值的计算方式是将Retained Set中的所有对象大小叠加。或者说,由于X被释放,导致其它所有被释放对象(包括被递归释放的)所占的heap大小。

Retained Set 当X被回收时那些将被GC回收的对象集合。

  1. MAT内存分析工具

具体使用可以参考这个链接介绍的较为详细:

MAT - Memory Analyzer Tool 使用进阶 http://www.lightskystreet.com/2015/09/01/mat_usage/

查看一个对象到RC Roots的引用链

  • List object - With outgoing References 显示选中对象持有那些对象

  • List object - With incoming Reference 显示选中对象被那些外部对象所持有

  • Show object by class - With outgoing References 显示选中对象持有哪些对象, 这些对象按类合并在一起排序

  • Show object by class - With incoming References 显示选中对象被哪些外部对象持有, 这些对象按类合并在一起排序

  • Merge Shortest path to GC root - exclude all phantom/weak/soft etc.references 通常在排查内存泄漏的时候,我们会选择exclude all phantom/weak/soft etc.references,意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump之前要手动出发GC来保证),如果有,则说明存在内存泄漏,然后再去排查具体引用。

Note:关于使用最短路径,其实最短路径不一定是内存泄漏的路径,实际操作时可以先查看所有incoming Reference查找比较有嫌疑的对象,比如我们自己的代码

常见内存泄漏和高内存占用原因

  1. 内部类

非静态内部类中线程生命周期不可控,能否正常回收完全由线程的生命周期决定。如果线程是永久运行的,那么将永远无法释放,因为在Java中线程是垃圾回收机制的根源,在运行系统中DVM虚拟机总会硬件持有所有运行状态的进程的引用,结果导致处于运行状态的线程将永远不会被回收。

非静态内部类还有一种的情况的内存泄漏 非静态内部类中创建了一个静态实例,导致该实例的生命周期和应用ClassLoader级别,又因为该静态实例又会隐式持有其外部类的引用,所以导致其外部类无法正常释放,出现了泄漏问题。

深入分析

参考这篇文章:

Android 非静态内部类导致内存泄漏原因深入剖析http://www.echojb.com/dotnet-report/2016/09/12/205273.html

解决思路

  • 去除隐式引用(通过静态内部类来去除隐式引用)

  • 手动管理对象引用(修改静态内部类的构造方式,手动引入其外部类引用)

  • 当内存不可用时,不执行不可控代码(Android可以结合智能指针,WeakReference包裹外部类实例)

  1. 上下文的引用

对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。

- 数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task,一般情况不推荐;

- 数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用;

- 数字3:在Receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视);

- ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。
  1. 其他
  • 慎重使用static变量 尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。

  • 长周期内部类、匿名内部类长时间持有外部类引用导致相关资源无法释放(Handler或者内部线程等)

  • BitMap导致内存溢出

  • 数据库、文件流等没有关闭

  • 监听器、广播注册后没有及时注销

  • 字符串拼接尽量使用StringBuilder或者StringBuffer(Java 8 不需要)

  • 避免内存抖动,例如不要在onDraw中创建对象。

  • 界面不可见时,停止动画和相关线程

  • 调用了View.getViewTreeObserver().addOnXXXListener ,而没调用View.getViewTreeObserver().removeXXXListener

  • Handler的持有的引用对象最好使用弱引用,资源释放时也可以清空Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler对象的Message和Runnable.removeCallbacks(Runnable r)removeMessages(int what),或removeCallbacksAndMessages(null)等。

  • 线程 Runnable 执行耗时操作,注意在页面返回时及时取消或者把 Runnable 写成静态类。 a) 如果线程类是内部类,改为静态内部类。 b) 线程内如果需要引用外部类对象如 context,需要使用弱引用。

GC的打印

Reason

名称 意义
Concurrent 后台回收内存,不暂停用户线程
Alloc 当app要申请内存,而堆又快满了的时候,会阻塞用户线程
Explicit 调用Systemt.gc()等方法的时候触发,一般不建议使用
NativeAlloc 当native内存有压力的时候触发

Name

Concurrent mark sweep—-全部对象的检测回收 Concurrent partial mark sweep—-部分的检测回收 Concurrent sticky mark sweep—-仅检测上次回收后创建的对象,速度快,卡顿少,比较频繁

比如: I/art: Explicit concurrent mark sweep GC freed 40184(1902KB) AllocSpace objects, 4(61KB) LOS objects, 22% free, 26MB/34MB, paused 840us total 90.643ms

freed 1413K表示GC释放了1902KB的内存 22% free, 26MB/34MB, 22%表示目前可分配内存占的比例,26MB表示当前活动对象所占内存,34MB表示Heap的大小 paused 840us, total 90.643ms,则表示触发GC应用暂停的时间和GC总共消耗的时间

参考资料

Java垃圾回收机制 http://youli9056.github.io/blog/categories/gc/
Android应用内存泄露分析、改善经验总结http://www.jianshu.com/p/33d3f89f7941
Android 内存泄漏总结https://yq.aliyun.com/articles/3009
Android内存泄漏分析及调试http://blog.csdn.net/gemmem/article/details/13017999