本文用于学习安卓垃圾回收所写,关于其中java垃圾回收的基础知识,可以翻看博主前三篇关于java虚拟机的文章。本文与上篇文章一起探讨Dalvik和ART的垃圾回收。
一、引子
Dalvik在mark阶段需要暂停应用线程两次,sweep阶段需要暂停一次,三次的STW开销带来了明显的卡顿,即使使用了Concurrent GC,让卡顿的时间非常短暂,但仍然无法做到尽善尽美。
ART改进后的垃圾回收算法只暂停线程一次。ART 能够做到这一点,是因为应用本身做了垃圾回收的一些工作。垃圾回收启动后,不再是三次暂停,而是一次暂停。在遍历阶段,应用不需要暂停,同时垃圾回收停时间也大大缩短,因为 Google使用了一种新技术(packard pre-cleaning),在暂停前就做了许多事情,减轻了暂停时的工作量。不过原理上仍然逃不出之前的铺垫,ART同样采用了自动GC的策略,并且同样不可避免的使用到了经典的mark-sweep算法。
二、ART的堆结构
2.1 内存分布
与Dalvik虚拟机垃圾收集机制一样,ART运行时垃圾收集机制也涉及到类似于Zygote堆、Active堆、Card Table、Heap Bitmap和Mark Stack等概念,如下图:
ART运行时堆划分为四个空间,分别是Image Space、Zygote Space、Allocation Space和Large Object Space。其中,Image Space、Zygote Space、Allocation Space是在地址上连续的空间,称为Continuous Space,而Large Object Space是一些离散地址的集合,用来分配一些大对象,称为Discontinuous Space。这个区域提高了GC的管理效率和整体性能。
2.2 共享机制
在Image Space和Zygote Space之间,隔着一段用来映射system@framework@boot.art@classes.oat
文件的内存。system@framework@boot.art@classes.oat
是一个OAT文件,它是由在系统启动类路径中的所有DEX文件翻译得到的,而Image Space空间就包含了那些需要预加载的系统类对象。这意味着需要预加载的类对象是在生成system@framework@boot.art@classes.oat
这个OAT文件的时候创建并且保存在文件system@framework@boot.art@classes.dex
中,以后只要系统启动类路径中的DEX文件不发生变化(即不发生更新升级),那么以后每次系统启动只需要将文件system@framework@boot.art@classes.dex
直接映射到内存即可,省去了创建各个类对象的时间。之前使用Dalvik虚拟机作为应用程序运行时时,每次系统启动时,都需要为那些预加载的类创建类对象。因此,虽然ART运行时第一次启动时会比较慢,但是以后启动实际上会更快。
由于system@framework@boot.art@classes.dex
文件保存的是一些预先创建的对象,并且这些对象之间可能会互相引用,因此我们必须保证system@framework@boot.art@classes.dex
文件每次加载到内存的地址都是固定的。这个固定的地址保存在system@framework@boot.art@classes.dex
文件开头的一个Image Header中。此外,system@framework@boot.art@classes.dex
文件也依赖于system@framework@boot.art@classes.oat
文件,因此也会将后者固定加载到Image Space的末尾。
Zygote Space和Allocation Space与Dalvik虚拟机垃圾收集机制中的Zygote堆和Active堆的作用是一样的。Zygote Space在Zygote进程和应用程序进程之间共享的,而Allocation Space则是每个进程独占的。同样的,Zygote进程一开始只有一个Image Space和一个Zygote Space。在Zygote进程fork第一个子进程之前,就会把Zygote Space一分为二,原来的已经被使用的那部分堆还叫Zygote Space,而未使用的那部分堆就叫Allocation Space。以后的对象都在Allocation Space上分配。
通过上述这种方式,就可以使得Image Space和Zygote Space在Zygote进程和应用程序进程之间进行共享,而Allocation Space就每个进程都独立地拥有一份。注意,虽然Image Space和Zygote Space都是在Zygote进程和应用程序进程之间进行共享,但是前者的对象只创建一次,而后者的对象需要在系统每次启动时根据运行情况都重新创建一遍。
三、ART的垃圾收集——仅以Concurrent Mark Sweep为例
在android源码中,ART的部分的GC在使用mark-sweep算法进行自动垃圾收集时,根据轻重程度不同,分为三类,快速GC策略Sticky GC;局部GC策略Partial GC;全局GC策略Full GC。可以看到,ART里的GC的改进,首先就是收集器的多样化。
而根据GC时是否暂停所有的线程分类并行和非并行两类。所以在ART中一共定义了6个mark-sweep收集器。这六种垃圾收集器分为两组。其中一组是支持并行GC的,另一组是不支持并行GC的。每一组都由MarkSweep、PartialMarkSweep和StickyMarkSweep三种类型的垃圾收集器组成。参看art/runtime/gc/heap.cc可见。根据不同情况,ART会选择不同的GC collector进行GC工作。其实最复杂的就是Concurrent Mark Sweep 收集器。如果理解了最复杂的Concurrent Mark Sweep算法,其他5种GC收集器的工作原理就也理解了。同样的,垃圾回收工作从整体上可以划分两个大的阶段:Mark 和 Sweep。
3.1 Mark阶段
最重要的提升就是这个阶段只暂停线程一次。将Dalvik的三次缩短到一次,得到了较大的优化。和Dalvik一样,标记阶段完成的工作也是完成从根集对象出发,进行递归遍历标记被引用的对象的整个过程。用到的主要的数据结构也是同样的live bitmap和mark bitmap, 以及card table和在递归遍历标记时用到的mark stack。
一个被引用的对象在标记的过程中先被标记,然后存入mark stack中,等待该对象的父对象全部被标记完成,再依次弹出栈中每一个对象然后,标记这个对象的引用,再把引用存入mark stack,重复这个过程直至整个栈为空。这个过程对mark stack的操作使用以及递归的方法和Dalvik的递归过程是一样的。但是在Dalvik小节里提到了,在标记时mark阶段划分成了两个阶段,第一小阶段是禁止其他线程执行的,在mark两个阶段完成后处理card table时也是禁止其他线程执行的。但是在ART里做出了改变,即先Concurrent标记两遍,也就是说两个子阶段都可以允许其他线程运行了。然后再Non-Concurrent标记一遍。这样就大大缩短了Dalvik里的第二次停顿的带来的卡顿时间。这个改进非常重要。
在标记开始阶段,有别于Dalvik的要暂停所有线程进行堆地址空间的遍历,ART去掉了这个过程,替代的是增加了一个叫作allocation stack结构,所有新分配的对象会记录到allocation stack,然后在Mark的时候,再在Live Bitmap中打上live的标记。Allocation stack和live stack其实是一个工作栈和备份栈。当在GC的时候,需要处理allocation stack,那么会把两个stack互换。新分配的对象会压到备份栈中,这个时候备份栈就当作新的工作栈。这样一来,Dalvik在GC时产生的第一次停顿就完全消除了,从而产生了巨大的性能提升。
关于card table,和Dalvik依旧类似,每个card用一个字节来描述。ART里多了一个结构ModUnionTable,是和card table配合使用的。
前面在ConCurrent的情况下,经过了两轮的递归遍历,基本上已经标记扫描的差不多了。但由于应用程序主线程是在一直运行的,不可避免地会修改之前已经mark过的bitmap。因此,需要第三遍扫描,这次就需要在stop the world的情况下进行遍历,主要过程也就是上篇文章提到的对card table的操作等等。
这次遍历扫的时候,除了重新标记根集以外,还需要扫描card table中Dirty Card的部分。关于live bitmap和mark bitmap的使用,ART和Dalvik在这一块没有多少区别。Live Bitmap记录了当前存在于VM进程中所有的未标记的对象和标记过的对象。Mark Bitmap经过了Mark 的过程,记录了当前VM进程中所有被引用的object。Live Bitmap和Mark Bitmap中间的差集,便是所有为被系统引用的object,即是可以回收的垃圾了。
由于Sweep的操作是对应于live bitmap,即那些在live bitmap中标记过,却在mark bitmap中没有标记的对象。也就是说,mark bitmap中标记的对象是live bitmap中标记对象的子集。但目前为止live bitmap标记的对象还不是最全,因为前文有提到过,为了消除Dalvik的第一次停顿,ART计入了allocation stack中的对象,还没有标记。Allocation stack先“搁置”起来不让后面的主线程使用,启用备份的的live stack。
3.1 Sweep阶段
在完成了mark阶段后,对应已经标好的live bitmap和mark bitmap,需要进入sweep来回收相应的垃圾。Sweep阶段就是把那些二者的差集所占用的内存回收掉。
四、小结
Dalvik的在GC时出现的几个主要问题,首先即在GC时会有三次暂停其他进程运行,三次停顿导致的总的时间太长会导致丢帧卡顿现象严重。其次,就是在堆空间中给较大的对象分配空间后会导致碎片化比较严重,并且可能会导致GC调用次数变多增加开销。
Dalvik的以上两个问题,ART都有做了对应的优化来解决这些问题。针对第一个问题,ART在标记阶段做了非常大的优化,消除了第一次遍历堆地址空间的停顿,和第二次标记根集对象的停顿,并缩短了第三次处理card table的停顿,因此大大的缩短了应用程序在执行时的卡顿时间。针对第二个问题,提出了LOS专门管理大对象的管理方法。
除此以外,还提供了丰富的GC收集器,例如继承自mark sweep的sticky mark sweep和partial mark sweep,二者的回收力度都要比full mark sweep小,因此性能上也得到了一些提升。一般情况下的收集器的主力就是sticky mark sweep, 这是对应用程序的性能影响最小的一种方式,因此大多数情况的GC表现,都要比Dalvik的GC表现更好。
以上都只是一个比较初步的分析比较,进一步的原理研究还需要详细学习源码才能融会贯通。
参考:
Android GC 从Dalvik到ART的改进分析 | cruise yang (cruise1008.github.io)
老罗博客