写点什么

Android-GC 原理探究(深度好文),PDF 超过 6000 页,

用户头像
Android架构
关注
发布于: 刚刚
  • GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的 GC。

  • GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发 GC 操作来释放内存。

  • GC_EXPLICIT: 表示是应用程序调用 System.gc、VMRuntime.gc 接口或者收到 SIGUSR1 信号时触发的 GC。

  • GC_BEFORE_OOM: 表示是在准备抛 OOM 异常之前进行的最后努力而触发的 GC。


实际上,GC_FOR_MALLOC、GC_CONCURRENT 和 GC_BEFORE_OOM 三种类型的 GC 都是在分配对象的过程触发的。而并发和非并发 GC 的区别主要在于前者在 GC 过程中,有条件地挂起和唤醒非 GC 线程,而后者在执行 GC 的过程中,一直都是挂起非 GC 线程的。并行 GC 通过有条件地挂起和唤醒非 GC 线程,就可以使得应用程序获得更好的响应性。但是同时并行 GC 需要多执行一次标记根集对象以及递归标记那些在 GC 过程被访问了的对象的操作,所以也需要花费更多的 CPU 资源。后文在 Art 的并发和非并发 GC 中我们也会着重说明下这两者的区别。

2.4 对象的分配和 GC 触发时机

1. 调用函数 dvmHeapSourceAlloc 在 Java 堆上分配指定大小的内存。如果分配成功,那么就将分配得到的地址直接返回给调用者了。函数 dvmHeapSourceAlloc 在不改变 Java 堆当前大小的前提下进行内存分配,这是属于轻量级的内存分配动作。


2. 如果上一步内存分配失败,这时候就需要执行一次 GC 了。不过如果 GC 线程已经在运行中,即 gDvm.gcHeap->gcRunning 的值等于 true,那么就直接调用函数 dvmWaitForConcurrentGcToComplete 等到 GC 执行完成就是了。否则的话,就需要调用函数 gcForMalloc 来执行一次 GC 了,参数 false 表示不要回收软引用对象引用的对象。


3. GC 执行完毕后,再次调用函数 dvmHeapSourceAlloc 尝试轻量级的内存分配操作。如果分配成功,那么就将分配得到的地址直接返回给调用者了。


4. 如果上一步内存分配失败,这时候就得考虑先将 Java 堆的当前大小设置为 Dalvik 虚拟机启动时指定的 Java 堆最大值,再进行内存分配了。这是通过调用函数 dvmHeapSourceAllocAndGrow 来实现的。


5. 如果调用函数 dvmHeapSourceAllocAndGrow 分配内存成功,则直接将分配得到的地址直接返回给调用者了。


6. 如果上一步内存分配还是失败,这时候就得出狠招了。再次调用函数 gcForMalloc 来执行 GC。参数 true 表示要回收软引用对象引用的对象。


7. GC 执行完毕,再次调用函数 dvmHeapSourceAllocAndGrow 进行内存分配。这是最后一次努力了,成功与事都到此为止。


示例图如下:



通过这个流程可以看到,在对象的分配中会导致 GC,第一次分配对象失败我们会触发 GC 但是不回收 Soft 的引用,如果再次分配还是失败我们就会将 Soft 的内存也给回收,前者触发的 GC 是 GC_FOR_MALLOC 类型的 GC,后者是 GC_BEFORE_OOM 类型的 GC。而当内存分配成功后,我们会判断当前的内存占用是否是达到了 GC_CONCURRENT 的阀值,如果达到了那么又会触发 GC_CONCURRENT。


那么这个阀值又是如何来的呢,上面我们说到的一个目标利用率,GC 后我们会记录一个目标值,这个值理论上需要再上述的范围之内,如果不在我们会选取边界值做为目标值。虚拟机会记录这个目标值,当做当前允许总的可以分配到的内存。同时根据目标值减去固定值(200~500K),当做触发 GC_CONCURRENT 事件的阈值。

2.5 回收算法和内存碎片

主流的大部分 Davik 采取的都是标注与清理(Mark and Sweep)回收算法,也有实现了拷贝 GC 的,这一点和 HotSpot 是不一样的,具体使用什么算法是在编译期决定的,无法在运行的时候动态更换。如果在编译 dalvik 虚拟机的命令中指明了"WITH_COPYING_GC"选项,则编译"/dalvik/vm/alloc/Copying.cpp"源码 – 此是 Android 中拷贝 GC 算法的实现,否则编译"/dalvik/vm/alloc/HeapSource.cpp" – 其实现了标注与清理 GC 算法。


由于 Mark and Sweep 算法的缺点,容易导致内存碎片,所以在这个算法下,当我们有大量


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


不连续小内存的时候,再分配一个较大对象时,还是会非常容易导致 GC,比如我们在该手机上 decode 图片,具体情况如下:



所以对于 Dalvik 虚拟机的手机来说,我们首先要尽量避免掉频繁生成很多临时小变量(比如说:getView,onDraw 等函数),另一个又要尽量去避免产生很多长生命周期的大对象。

3、ART 内存回收机制

3.1 Java 堆

ART 运行时内部使用的 Java 堆的主要组成包括 Image Space、Zygote Space、Allocation Space 和 Large Object Space 四个 Space,Image Space 用来存在一些预加载的类, Zygote Space 和 Allocation Space 与 Dalvik 虚拟机垃圾收集机制中的 Zygote 堆和 Active 堆的作用是一样的,


Large Object Space 就是一些离散地址的集合,用来分配一些大对象从而提高了 GC 的管理效率和整体性能,类似如下图:



在下文的 GC Log 中,我们也能看到在 art 的 GC Log 中包含了 LOS 的信息,方便我们查看大内存的情况。

3.2 GC 的类型

  • kGcCauseForAlloc ,当要分配内存的时候发现内存不够的情况下引起的 GC,这种情况下的 GC 会 stop world

  • kGcCauseBackground,当内存达到一定的阀值的时候会去出发 GC,这个时候是一个后台 gc,不会引起 stop world

  • kGcCauseExplicit,显示调用的时候进行的 gc,如果 art 打开了这个选项的情况下,在 system.gc 的时候会进行 gc

  • 其他更多

3.3 对象的分配和 GC 触发时机

由于 Art 下内存分配和 Dalvik 下基本没有任何区别,我直接贴图带过了。


3.4 并发和非并发 GC

Art 在 GC 上不像 Dalvik 仅有一种回收算法,Art 在不同的情况下会选择不同的回收算法,比如 Alloc 内存不够的时候会采用非并发 GC,而在 Alloc 后发现内存达到一定阀值的时候又会触发并发 GC。同时在前后台的情况下 GC 策略也不尽相同,后面我们会一一给大家说明。


  • 非并发 GC


步骤 1. 调用子类实现的成员函数 InitializePhase 执行 GC 初始化阶段。


步骤 2. 挂起所有的 ART 运行时线程。


步骤 3. 调用子类实现的成员函数 MarkingPhase 执行 GC 标记阶段。


步骤 4. 调用子类实现的成员函数 ReclaimPhase 执行 GC 回收阶段。


步骤 5. 恢复第 2 步挂起的 ART 运行时线程。


步骤 6. 调用子类实现的成员函数 FinishPhase 执行 GC 结束阶段。


  • 并发 GC


步骤 1. 调用子类实现的成员函数 InitializePhase 执行 GC 初始化阶段。


步骤 2. 获取用于访问 Java 堆的锁。


步骤 3. 调用子类实现的成员函数 MarkingPhase 执行 GC 并行标记阶段。


步骤 4. 释放用于访问 Java 堆的锁。


步骤 5. 挂起所有的 ART 运行时线程。


步骤 6. 调用子类实现的成员函数 HandleDirtyObjectsPhase 处理在 GC 并行标记阶段被修改的对象。。


步骤 7. 恢复第 4 步挂起的 ART 运行时线程。


步骤 8. 重复第 5 到第 7 步,直到所有在 GC 并行阶段被修改的对象都处理完成。


步骤 9. 获取用于访问 Java 堆的锁。


步骤 10. 调用子类实现的成员函数 ReclaimPhase 执行 GC 回收阶段。


步骤 11. 释放用于访问 Java 堆的锁。


步骤 12. 调用子类实现的成员函数 FinishPhase 执行 GC 结束阶段。


所以不论是并发还是非并发,都会引起 stopworld 的情况出现,并发的情况下单次 stopworld 的时间会更短,基本区别和。

3.5 Art 并发和 Dalvik 并发 GC 的差异

首先可以通过如下 2 张图来对比下


Dalvik GC:



Art GC



Art 的并发 GC 和 Dalvik 的并发 GC 有什么区别呢,初看好像 2 者差不多,虽然没有一直挂起线程,但是也会有暂停线程去执行标记对象的流程。通过阅读相关文档可以了解到 Art 并发 GC 对于 Dalvik 来说主要有三个优势点:


1、标记自身


Art 在对象分配时会将新分配的对象压入到 Heap 类的成员变量 allocation_stack_描述的 Allocation Stack 中去,从而可以一定程度缩减对象遍历范围。


2、预读取


对于标记 Allocation Stack 的内存时,会预读取接下来要遍历的对象,同时再取出来该对象后又会将该对象引用的其他对象压入栈中,直至遍历完毕。


3、减少 Pause 时间


在 Mark 阶段是不会 Block 其他线程的,这个阶段会有脏数据,比如 Mark 发现不会使用的但是这个时候又被其他线程使用的数据,在 Mark 阶段也会处理一些脏数据而不是留在最后 Block 的时候再去处理,这样也会减少后面 Block 阶段对于脏数据的处理的时间。

3.6 前后台 GC

前台 Foreground 指的就是应用程序在前台运行时,而后台 Background 就是应用程序在后台运行时。因此,Foreground GC 就是应用程序在前台运行时执行的 GC,而 Background 就是应用程序在后台运行时执行的 GC。


应用程序在前台运行时,响应性是最重要的,因此也要求执行的 GC 是高效的。相反,应用程序在后台运行时,响应性不是最重要的,这时候就适合用来解决堆的内存碎片问题。因此,Mark-Sweep GC 适合作为 Foreground GC,而 Mark-Compact GC 适合作为 Background GC。


由于有 Compact 的能力存在,碎片化在 Art 上可以很好的被避免,这个也是 Art 一个很好的能力。

3.7 Art 大法好

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android-GC原理探究(深度好文),PDF超过6000页,