课程名称:Top团队大牛带你玩转Android性能分析与优化
课程章节:App性能概览与平台化实践
主讲老师:随风绽放
课程内容
1.内存问题介绍及优化工具选择
由于 Java 的 JVM 引入了垃圾回收机制,垃圾回收器会自动回收不再使用的对象,使得平时对内存问题投入的关注程度不够。内存问题又是一个大问题,出现内存问题是一个长期累积的结果,需要我们投入更多的关注。
常见的内存问题有三种:
- 内存抖动,指在短时间内有大量的对象被创建或者被回收的现象,频繁内存抖动会导致垃圾回收频繁运行,造成系统卡顿。
- 内存泄露,没用的 Java 对象因为还被引用着,导致垃圾回收器没办法移除它们,依旧占用着内存。
- 内存溢出,App 要使用的内存超过了 JVM 能提供的最大内存,造成 App 崩溃。
常用的内存优化工具也有三种:
- Memory Profiler,是 AndroidStudio 自带的检测工具,通过实时的图表展示了内存的使用量,还提供了捕获堆转储、强制GC以及跟踪内存分配的能力。特点是方便直观,但只能在开发阶段使用,无法带到线上。
- Memory Analyzer(MAT),强大的Java Heap 分析工具,查找内存泄漏及内存占用情况。没有 MemoryProfiler 直观,但功能更强大,能够生成整体报告和分析问题报告等。也只能在线下使用,通常使用 MemoryProfiler 进行内存问题的初步定位,使用 MAT 详细分析。
- LeakCanary,自动内存泄漏检测工具,需要手动集成,在线下使用,leakcanary占用内存很大,可以作为前两个工具的补充。
2.Java 和 Android 的内存管理机制
Java 的内存管理
JVM 管理的内存大致包括三种不同类型的内存区域:
- Permanent Generation space(永久保存区域),永久保存区域主要存放Class(类)和Meta的信息,Class 第一次被 Load 的时候被放入永久保存区域,Class 需要存储的内容主要包括方法和静态属性。
- Heap space(堆区域),堆区域用来存放 Class 的实例(即对象),对象需要存储的内容主要是非静态属性。每次用 new 创建一个对象实例后,对象实例存储在堆区域中,这部分空间也被 JVM 的垃圾回收机制管理。
- Java Stacks(Java栈),Java栈主要存储基本类型变量以及方法的输入输出参数。Java程序的每个线程中都有一个独立的堆栈。容易发生内存溢出问题的内存空间包括:永久保存区域和堆区域。
Java 内存的回收算法
1.标记清除算法
标记出所有需要回收等对象,统一回收所有被标记的对象,标记清除算法需要标记所有的内存块,效率不高,此外还会产生大量不连续的内存碎片。
2.复制算法,将内存划分为大小相等的两块,一块用完之后,复制存活对象到另一块,清理另一块内存,示意图如下,复制算法实现简单,效率高,只需要对所有内存的1/2进行标记,没有过多碎片,但浪费一半内存空间,代价大。
3.标记-整理算法
标记过程与“标记-清除”算法一样,存活对象往一端进行移动,清理其余内存。标记-整理算法避免标记,清除导致的内存碎片,也没有复制算法的空间浪费问题。
4.分代收集算法
结合多种收集算法优势(将其应用不同生命周期),新生代对象存活率低,使用复制算法,复制一定比例,老年代对象存活率高,使用标记-整理。
Android 的内存管理机制
Android 的内存采用弹性分配的原则,内存的分配值和最大值收到具体设备的影响。通常发生内存溢出(OOM)有两种情况,一种是设备本身的内存不够,导致 app 分配不到使用所需的内存。另一种情况是启动了多个 app,使得系统的可用内存不足。
Android 并没有直接使用 Java 的 JVM,而是有自己的一套虚拟机。在 Android5.0 之前是 Dalvik,Android5.0 之后是 Art。Dalvik仅固定一种回收算法,Art回收算法可运行期选择,比如App 在前台时,对响应速度要求高,可能会使用标记-清除算法。App在后台时,使用标记-整理算法。此外 Art 还具备内存整理能力,减少碎片的产生。
最后 Android 还有一套 Low Memory Killer机制,Android 将进程按照优先级高低依次划分为前台进程、可见进程、服务进程、空进程。前面优先级最高,后面最低,当内存不足时,优先回收低优先级进程。在回收内存时,还会考虑回收的收益,比如回收进程 A 可以得到 300M 的内存,回收进程 B 可以得到 10K 的进程,这是就会考虑回收 A 进程。
3.线上内存监控方案
内存泄露会引起内存溢出和内存抖动,线上内存监控的重点是监测内存泄露问题。
常规方案一
可以预先设定 app 的场景,比如当前内存的使用已经占用到单个 app 可用内存的 80%,达到一个高内存使用的状态,这是可以通过 Dump,具体是调用 Debug.dumpHprofData(),将当前的内存信息转化成文件,然后回传生成的文件到后台,开发人员通过 MAT 手动分析文件来定位内存泄露。
由于 Dump 生成的文件是和当前内存中的对象数相关的,生成的文件会很大,导致回传文件的失败率升高,可以采用 Hook 的方式对文件进行裁剪,减小文件的体积。
方案一配合一定的策略,对监控内存泄露有一定的效果,但需要预设可能出现内存泄露的场景,而且需要考虑回传文件失败的可能。
常规方案二
将 LeakCanary 带到线上,LeakCanary 发现内存泄露大致经历下面几个过程:
- 监测 Activity 的生命周期的 onDestroy() 的调用;
- 当某个 Activity 的 onDestroy() 调用后,便对这个 Activity 创建一个带 ReferenceQueue 的弱引用,并且给这个弱引用创建了一个 key 保存在 Set集合中;
- 如果这个 Activity 可以被回收,那么弱引用就会被添加到 ReferenceQueue 中。
- 等待主线程进入 idle (即空闲)后,通过一次遍历,在 ReferenceQueue 中的弱引用所对应的 key 将从 RetainedKeys 中移除,说明其没有内存泄漏。
- 如果 Activity 没有被回收,先强制进行一次 GC,再来检查,如果 key 还存在 RetainedKeys 中,说明 Activity 不可回收,同时也说明了出现了内存泄漏。
- 发生内存泄露之后,通过 Dump 内存快照,生成 .hprof 文件。
由于 LeakCanary 的分析内存泄露的路径比较长,分析过程中占用内存比较大。我们可以对 LeakCanary 进行定制,只分析Retain size 大的对象,不是所有的都进行分析。对 Dump 生成的内存快照进行裁剪,而是全部加到内存。
通过以上两种方案的介绍,我们可以指定一套完整的线上内存监控方案。对常规内存指标的监测,包括待机内存、重点模块内存占用、OOM率等,此外还要监控 App 一个完整生命周期和重点模块的 GC 次数和 GC 时间。通过定制化的 LeakCanary 来定位和分析内存泄露。
课程收获
这一章内容介绍了内存抖动、内存泄露和内存溢出三种常见的内存使用问题。其中内存泄露是我们需要关注的重点。这一章还详细介绍了三种内存使用分析工具,并给出了一套适合线上的内存监控方案。