周所周知,Java虚拟机拥有GC技术,Android 也是如此,我们不需要在代码中显式的去分配与释放内存。

除此之外,Andriod 系统在内存管理上有一个 Generational Heap Memory(分代堆内存)模型,当内存达到某一个阈值时,系统会根据不同的规则自动释放系统认为可以释放的内存。

但正是因为安卓把内存控制交给了这个模型,所以当出现了内存泄露等方面的问题,排查就会很艰难,另一方面,内存的不合理使用也会造成很多性能问题,比如短时间分配大量的内存对象吗,就算没有内存泄露,但是占用也会很高。

所以当开发者在开发复杂业务时,如果完全依赖于自身的内存管理,那就会导致一系列的性能问题。

1 Android 内存管理机制

安卓应用都是在虚拟机中进行,安卓系统中的虚拟机有两种运行模式:Dalvik和ART。

1.1 Java 对象生命周期

  1. 创建阶段

    1. 给对象分配存储空间

    2. 构造对象

    3. 从超类到子类对 static成员进行初始化,类的初始化在 类加载器加载该类是进行。

    4. 超类成员变量按顺序初始化,递归调用超类的构造方法。

    5. 子类成员变量按顺序初始化,一但对象被创建,子类构造方法就调用该对象并为变量赋值。

  2. 应用阶段

    对象至少被一个强引用持有,除非在系统中显示的使用了其他强度的引用。

    软引用可以加速虚拟机对于垃圾内存的回收,强弱软虚四种引用, 大家可以去网站上搜索了解。

  3. 不可见阶段

    在这个阶段的对象在虚拟机的对象引用根集合中找不到直接或者间接的强引用,这些对象一般是所有线程栈中的临时变量,所有已经装载的静态变量或者是贝对本地代码接口的引用。

    当一个对象处于这个阶段的时候,说明程序本身已经不持有这个对象了,但是该对象还是存在的。比如有可能被其他程序强行持有。这种特殊的强应用就成为GC root。存在这些 GC Root 就会导致对象的内存泄露,无法被收回。

  4. 不可达阶段

    对象处于不可达阶段是指该对象不被任何强应用持有,回收器发现该对象不可达,这个是可以被回收的对象。

  5. 收集阶段

    当垃圾回收器发现对象已经处于不可达阶段,并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,对象就进入收集阶段,如果对象重写了 finalize()方法,那就执行完这个方法之后进行下一阶段。

  6. 终结阶段

    当对象执行完 finalize 方法之后,如果还是根不可达状态,那就进入该阶段,等待被回收。

  7. 对象空间重新分配

    GC 对对象占用的内存重新回收与分配。

    其实在垃圾回收标准中,当我们不需要再使用对象时,我们应该将对象置空,这样子可以提高内存使用效率,也不要采用过深的继承。访问本地变量优于访问类中的变量。

1.2 内存分配

我们要先了解堆的概念,堆就是一块匿名共享内存。安卓虚拟机并没有直接去管理这块内存,而是封装成一个mSpace,通过底层C库来管理,并且使用 libc 提供的函数 malloc 和 free 来分配和释放内存。

安卓应用的进程都是通过 Zygote 进程用 fork 方法衍生出来,这就使得大多数的内存用来分配给 Framework 代码,同时促进 RAM 资源应用的所有进程之间共享。

大多数静态资源数据被映射到一个进程中。这不仅让同样的数据能在数据中共享,也使得在他能够在需要的时候共享使用。常见的静态资源包括 Dalvik Code,app Resoures,so文件等等。

大多数情况下,安卓通过显示分配内存共享区域(如 ashmem 或者 gralloc)来实现动态内存区域能够在不同进程之间共享的机制。比如,Window Surface 在 APP 与 Screen Compositor 之间使用共享的内存,Cursor Buffer 在 Content Provider 与 Client 之间共享内存。

安卓系统为每一个应用程序都设置了一个 Dalvik Heap Size 的最大显示阈值,这个阈值在不同的设备上会因为 RAM 大小而有所差异。所以应用使用的内存已经很接近这个最大值但是还去尝试分配内存的话,就容易引发 OOM 错误。

ActivityManager.getMemoryClass ()可以⽤来查询当 前应⽤的 Heap Size 阈值,这个⽅法会返回⼀个整数,表明应⽤的 Heap Size 阈值是多少 MB 。

接下来我们看看前面说过的 Dalvik 和 ART 两种方式有什么不同,首先他们的内存区域块是不同的,如下:

我们发现两种模式,都划分为三个空间:LinearAlloc,Zygote Space(Zygote Heap)和 Allocation Space(Active Heap)。Dalvik 中的 LinearAlloc 是一个线性内存空间,并且是只读的,主要用来存储虚拟机中的类,因为类加载后就只用读,把这些数据放在线性分配器中去管理,就能很好的减少混乱与垃圾扫描,提高内存管理性能。

ART 虚拟机 Zygote Space 和 Allocation Space 与 Dalvik 虚拟机中的 Zygote Space 和 Allocation Space ⼀样。 Zygote Space 在 Zygote 进程和应⽤ 程序进程之间共享, Allocation Space 则是每个进程独占。 Android 系统 的第⼀个虚拟机由 Zygote 进程创建并且只有⼀个 Zygote Space 。但是当 Zygote 进程在 fork 第⼀个应⽤程序进程之前,会将已经使⽤的那部分堆 内存划分为⼀部分,还没有使⽤的堆内存划分为另外⼀部分,也就是 Allocation Space 。但⽆论是应⽤程序进程,还是 Zygote 进程,当它们需 要分配对象时,都是在 Allocation Space 堆上进⾏。在 ART 运⾏时,堆除了 Zygote Space 、 Allocation Space ,⼜多了两 个空间,即 Image Space 和 Large Object Space 。其中 Image Space ⽤来存 放⼀些预加载类,和 Dalvik 中的 Linear Alloc 类似,⽽ Large Object Space 是⼀些离散地址的集合,⽤来分配⼀些⼤对象,这样可以提⾼ GC 的管 理效率和整体性能。其中 Image Space 和 Zygote Space 在 Zygote 进程和应 ⽤程序进程之间共享,⽽ Allocation Space 是每个进程都独⽴拥有⼀份。

Image Space 和 Zygote Space 都是在 Zygote 进程和应⽤程 序进程之间共享,但是 Image Space 的对象只创建⼀次,⽽ Zygote Space 的对象需要在系统每次启动时,根据运⾏情况都重新创建⼀遍

1.3 内存回收机制

前面提过一个分代回收模型,下面是具体的图:

由于我之前是学 java 方面,所以这方面有所涉猎,如有需要,我会之后开新专栏来讲解。

在 Android 系统中, GC 有以下三种类型:

  • kGcCauseForAlloc :在分配内存时发现内存不够的情况下引起的 GC ,这种情况下的 GC 会 Stop World 。 Stop World 是由于并发 GC 时,其 他线程都会停⽌,直到 GC 完成。

  • kGcCauseBackground :当内存达到⼀定的阈值时触发 GC ,这个 时候是⼀个后台 GC ,不会引起 Stop World 。

  • kGcCauseExplicit :显式调⽤时进⾏的 GC ,如果 ART 打开了这个 选项,在 system.gc 时会进⾏ GC 。

下⾯来看⼀段虚拟机打印出来的⽇志:

D/dalvikvm( 7030): GC_CONCURRENT freed 1049K, 60% free 2341K/9351K, external 3502K/6261K, paused 3ms 3ms

GC_CONCURRENT 是当前 GC 时的类型,在 Android 的虚拟机中 GC ⽇志有以下⼏种类型:

  • GC_CONCURRENT :当应⽤进程中的 Heap 内存占⽤上涨时,避 免因 Heap 内存满了⽽触发 GC 。

  • GC_FOR_MALLOC :这是由于 Concurrent GC 没有及时执⾏完, ⽽应⽤⼜需要分配更多的内存,这时不得不停下来进⾏ Malloc GC 。

  • GC_EXTERNAL_ALLOC :这是为 external 分配的内存执⾏的 GC 。

  • GC_HPROF_DUMP_HEAP :创建⼀个 HPROF profile 的时候执 ⾏。

  • GC_EXPLICIT :显式地调⽤了 System.gc ()。

⼀般来说,可以 信任系统的 GC 机制,尽量不去显式调⽤ System.gc (),减少不必要的 系统开销,影响应⽤的流畅度。 再回到上⾯打印出的⽇志,其中: freed 1049K 表明在这次 GC 中回收了多少内存。 60%free 3571K/9991K 是 Heap 的⼀些统计数据,表明这次回收后 60% 的 Heap 可⽤,存活的对象⼤⼩为 2341KB , Heap ⼤⼩是 9351KB 。 external 3502K/6261K 是 Native Memory 的数据。存放位图数据 ( Bitmap Pixel Data )或者堆以外内存( NIO Direct Buffer )之类的。 第⼀个数字表明 Native Memory 中已分配了多少内存,第⼆个值有点类 似⼀个浮动的阈值,表明分配内存达到这个值,系统就会触发⼀次 GC 进⾏内存回收。 paused 3ms 3ms 表明 GC 暂停的时间。从这⾥可以看到,越⼤的 Heap Size 在 GC 时导致暂停的时间越⻓。如果是 Concurrent GC ,会看 到两个时间:⼀个开始,⼀个结束,且时间很短,但如果是其他类型 的 GC ,很可能只会看到⼀个时间,且这个时间是相对⽐较⻓的。 注意 在 ART 模式下⽇志多了⼀个 Large Object Space (⼤对 象占⽤的空间),这部分内存并不是分配在堆上的,但仍属于应⽤程 序内存空间,主要⽤来管理 Bitmap 等占内存⼤的对象,避免因分配⼤ 内存导致堆频繁 GC 。

注意:Dalvik 虚拟机下 GC 的操作都是并发的,也就是都会导致 STW。但是 ART 下不一定都是并发的,比如 Alloc 内存不够的时候会采用非并发的 GC。

1.4 内存优化的意义

有时候 CG 其实是发生了的,但是由于时间很短,我们没有察觉,这种问题积少成多,就会出现问题。

OOM 的主要原因是,申请的内存还是不满足于程序使用。

还有,虽然开发者不用担心对象创建和销毁时候的操作,但是,内存泄露的问题还是会存在,当累计多了之后,每次申请的内存就会变少,所以也会间接的导致频繁 GC。

除了导致卡顿与 OOM,在应用程序后台运行的时候,就算提高优先级,如果内存占用过高,也会出现被 kill 的情况。

  • 减少 OOM ,提⾼应⽤稳定性。

  • 减少卡顿,提⾼应⽤流畅度。

  • 减少内存占⽤,提⾼应⽤后台运⾏时的存活率。

  • 减少异常发⽣,减少代码逻辑隐患。

2 内存分析工具

2.1 Memory Monitor

Memory Monitor 是⼀款使⽤⾮常简单的图形化⼯具,可以很好地 监控系统或应⽤的内存使⽤情况,主要有以下⼏个功能:

  • 显⽰可⽤和已⽤内存,并且以时间为维度实时反应内存分配和回 收情况。

  • 快速判断应⽤程序的运⾏缓慢是否是由于过度的内存回收导致。

  • 快速判断应⽤是否是由于内存不⾜导致程序崩溃。

通过观察以时间为维度实时反应内存分配和回收情况,可以快速 发现内存抖动、⼤内存分配,甚⾄由于 CG 导致卡顿。

  1. 使用介绍

    首先肯定是打开开发者模式,usb 调试了。

    1. 在 android studio 中运行需要监控的应用。

    2. 在菜单栏中选择 tools -> android -> memory monitor

    3. 一但开始运行,图形就会实时显示当前的内存使用情况

  2. 典型场景

    介绍两个基本场景:

    1. 内存分配与释放

      清晰可知,前面 GC,之后进行了分配。

    2. 大内存申请与内存抖动

      内存抖动:是指在很短的时间里,发生了多次内存分配与释放,当发生严重抖动时,也能感觉到卡顿。

2.2 Heap Viewer

Heap Viewer 的主要功能是查看不同数据类型在内存中的使⽤情况,可以看到当前进程中的 Heap Size 的情况,分别有哪些类型的数据,以及各种类型数据占⽐情况。通过分析这些数据来找到⼤的内存对象,再进⼀步分析这些⼤对象,进⽽通过优化减少内存开销,也可以通过数据的变化发现内存泄漏。

目前只在 5.0 以上支持,并且需要开发者模式。

  1. 启动

    在 android studio 中的 Tools -> Android -> Android Device Monitor 命令。

    进入面板之后,在进程列表中,选择需要查看的进程,单击 Update Heap 按扭。

    右边就开始更新数据,数值会在每一次 GC 之后更新。包括应用自动触发或者在面板上手动触发。

    不仅可以检测内存泄露,也能检测是否内存抖动,如果抖动,可以发现数据在频发更新。

  2. 观察面板

    一共三个面板

    1. 总览区(A)

      查看整体内存的使用情况,包括已经使用和未使用内存的占比。

    2. 详情页(B)

      可以看见各种类型的内存开销。

    3. 具体类型内存分配柱状图(C)

      可以查看选中的类型的内存分析情况。

    接下来我们来了解一下 A区 中数值对应的关系:

    • Heap Size:堆栈分配给 App 的内存大小。

    • Allocated:已分配使用的内存大小。

    • Free:空闲的内存大小。

    • Used(%):Allocated/Heap Size,使用率。

    • Object:对象数量。

    再来了解一下 B区 中的对应关系:

    • free:空闲的对象。

    • data object:数据对象,Java类 类型对象,是主要的观察对象。

    • class object:Java类 类型的应用对象。

    • 1-byte array:一字节的数组对象。

    • 2-byte array:两字节的数组对象。

    • 4-byte array:四字节的数组对象。

    • 8-byte array:一字节的数组对象。

    • non-java object:非 java对象。

    再了解一下每个类型对应关系:

    • Count:数量

    • Total Size:总共占用的内存大小。

    • Smallest:将对象占用内存从小到大排列,排在第一个的对象内存的大小。

    • Largest:将对象占用内存从小到大排列,排在最后一个的对象内存的大小。

    • Median:将对象占用内存从小到大排列,排在最后一个的对象内存的大小。

    • Average:平均值。

    选择一个具体类型之后,会显示对应的内存柱状图,横坐标是内存大小,纵坐标是对象的数量。

2.3 Allocation Tracker

Memory Monitor 和 Heap Viewer 都可以很直观且实时地监控内存使⽤情况,还能发现内存问题,但发现内存问题后不能再进⼀步找到原因,或者发现⼀块异常内存,但不能区别是否正常,同时在发现问题后,也不能定位到具体的类或⽅法。这时就需要使⽤另⼀个内存分析⼯具 Allocation Tracker 进⾏更详细的分析, Allocation Tracker 可以分配跟踪记录应⽤程序的内存分配,并列出了它们的调⽤堆栈,可以查看所有对象内存分配的周期。

主要功能如下:

  • 在⼀段时间内以对象类型为纬度,跟踪在此时间内的内存分配和 释放情况。

  • 寻找代码中内存使⽤不合理的地⽅。

Allocation Tracker 是分析较短⼀段时间内的内存使⽤情况,在使⽤ Allocation Tracker 前,可以先⽤ Memory Monitor 或者 Heap Viewer 找到内存异常的场景,然后使⽤ Allocation Tracker 分析这个场景的内存使⽤情况。

  1. 使用

    1. 点击启动追踪按钮(Start Allocation tracking)

    2. 操作应用,也就是重现场景的操作。

    3. 点击结束追踪

    4. 会生成一个 alloc 结尾的文件,并打开面板。

  2. 查看面板信息

    上面是内存对象列表,下面是对象引用堆栈。

    1. 内存对象列表

      列名

      数据意义

      Allocation Order

      内容分配序列

      Allocated Class

      被分配的内存对象

      Allocation Size

      分配的内存大小

      Thread ID

      分配该内存的线程 ID

      Allocation Site

      分配该对象的方法

    2. 对象引用堆栈

      在内存对象列表中,选中某⼀个对象后,在下⾯的窗⼝显⽰调⽤堆栈,单击具体的堆栈可以进⼊具体的代码⾏。

这个工具可以方便的提供程序在某一段时间里面的内存分配情况,并且可以跟踪到具体代码,但是不能提供任何 Heap 的总体情况和 Heap 的具体信息。之后会介绍一个更强大的内存分析工具:MAT

3 避免内存泄露

开发者在应⽤开 发阶段更重视功能需求的开发,却忽略了内存是否合理使⽤以及使⽤完如何处理,进⽽导致在不知不觉产⽣了内存泄漏的情况。

内存泄漏是指应⽤不再使⽤的内存对象,但垃圾回收时没有把这些辨认出来,不能及时回收,⼀直保留在内存中⻓期占⽤⼀定的空间,再也不释放给其他对象。

3.1 使用 MAT 查找内存泄露

Memory Analyzer Tool ( MAT )是⼀个快速、功能丰富的 Java Heap 分析⼯具,通过分析 Java 进程的内存快照 HPROF ⽂件,从众多的对象中分析,快速计算出在内存中对象的占⽤⼤⼩,查看哪些对象不能被垃圾收集器回收,并可以通过视图直观地查看可能造成这种结果的对象。

  1. 下载 MAT 客户端

  2. 获取 HPROF 文件

    从 Android Studio 进⼊ Android Device Monitor ( DDMS ),选择需要分析的应⽤进程,单击 Update Heap 按钮,对应⽤进⾏怀疑有内存问题的操作,也可以整体操作⼀段时间,结束操作后,多主动进⾏⼏次 GC ,最后单击 Dump HPROF File 按钮,保存 HPROF ⽂件。

  3. 转换格式

    因为 Android Studio 保存的是 Android Dalvik 格式 .hprof ⽂件,所 以需要转换成 J2SE HPROF 格式才能被 MAT 识别和分析。 Android SDK ⾃带⼀个转换⼯具 hprof-conv ,转换语句如下:

    ./hprof-conv path/file.hprof exitPath/heap-converted.hprof

    其中 path 为转换前的⽂件路径, exitPath 为转换后⽂件的路径。

  4. 打开转换后文件

    在 android studio 高版本中有便捷的方式去获取并转换文件。