BiBi - Android VM -2- ART

参考:罗升阳的相关博客
https://blog.csdn.net/Luoshengyang/article/details/42072975

1. 简介

  • 堆划分

ART运行时堆划分为四个空间,分别是:
Image Space【连续】【共享】【创建一次】【不会被回收】
Zygote Space【连续】【共享】【启动创建】【Full GC时回收】
Allocation Space【连续】【每次GC都回收】
Large Object Space【不连续】【每次GC都回收】
其中,Image Space、Zygote Space、Allocation Space是在地址上连续的空间,称为Continuous Space,而Large Object Space是一些离散地址的集合,用来分配一些大对象,称为Discontinuous 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进程和应用程序进程之间进行共享,但是前者的对象只创建一次,而后者的对象需要在系统每次启动时根据运行情况都重新创建一遍。

  • classes.oat 和 classes.dex

在Image Space和Zygote Space之间,隔着一段用来映射system@framework@boot.art@classes.oat文件的内存。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运行时第一次启动时会比较慢,但是以后启动实际上会更快。

由于classes.dex文件保存的是一些预先创建的对象,并且这些对象之间可能会互相引用,因此我们必须保证classes.dex文件每次加载到内存的地址都是固定的。这个固定的地址保存在classes.dex文件开头的一个Image Header中。此外,classes.dex文件也依赖于classes.oat文件,因此也会将后者固定加载到Image Space的末尾。

  • 垃圾收集器【GarbageCollector】

MarkSweep【回收Zygote Space、Allocation Space和Large Space上的对象,不会对上次GC以后分配的对象进行回收】
PartialMarkSweep【回收在Allocation Space和Large Space上分配的对象】
StickyMarkSweep【回收上次GC以来在Allcation Space上分配的最终又没有被引用的垃圾,不回收Large Space上的对象】

上面三种类型的垃圾收集器又分别有并发和非并发的,所以共有六种类型。【通过不同的垃圾收集策略,就有可能以最小代价解决分配对象时遇到的内在不足的问题】

继承关系:
StickyMarkSweep继承于PartialMarkSweep,PartialMarkSweep又继承于MarkSweep、而MarkSweep又继承于GarbageCollector。
因此,我们可以推断出,GarbageCollector定义了垃圾收集器接口,而MarkSweep、PartialMarkSweep和StickyMarkSweep通过重定某些接口来实现不同类型的垃圾收集器。

  • GarbageCollector描述GC的各个阶段
     GarbageCollector通过定义以下五个虚函数描述GC的各个阶段
     1. InitializePhase: 用来实现GC的初始化阶段,用来初始化垃圾收集器内部的状态。
     2. MarkingPhase: 用来实现GC的标记阶段,该阶段有可能是并行的,也有可能不是并行。
     3. HandleDirtyObjectsPhase: 用来实现并行GC的Dirty Object标记,也就是递归标记那些在并行标记对象阶段中被修改的对象。
     4. ReclaimPhase: 用来实现GC的回收阶段。
     5. FinishPhase: 用来实现GC的结束阶段。
     MarkSweep类通过重写上述五个虚函数实现自己的垃圾收集过程,同时,它又通过定义以下三个虚函数来让子类PartialMarkSweep
     和StickyMarkSweep实现特定的垃圾收集器:
     1. MarkReachableObjects: 用来递归标记从根集对象引用的其它对象。
     2. BindBitmap: 用来指定垃圾收集范围。
     3. Sweep: 用来回收垃圾对象。 
     其中,MarkSweep类通过自己实现的成员函数BindBitmap将垃圾收集范围指定为Zygote和Allocation空间,而PartialMarkSweep
     和StickyMarkSweep类通过重写成员函数BindBitmap将垃圾收集范围指定为Allocation空间和上次GC后所分配的对象。此外,StickyMarkSweep
     类还通过重定成员函数MarkReachableObjects和Sweep将对象标记和回收限制为上次GC后所分配的对象。
     ~
    
  • Space

Continuous Space类有两个成员变量begin_和end_,用来描述一个Continuous Space内部所使用的内存块的开始和结束地址。Discontinuous Space在地址空间上是不连续的,因此它不像Continuous Space一样,可以使用类似begin_和end_的成员变量来确定Space内部使用的内存块。Discontinuous Space类在内部使用两个SpaceSetMap容器live_objects_和mark_objects_来描述已经分配对象集合和在GC过程中被标记的对象集合。

Continuous Space内部使用的内存块都是通过内存映射得到的,不过这块内存有可能是通过不同方式映射得到的。例如,Image Space内部使用的内存块是通过内存映射Image文件得到的,而Zygote Space和Allocation Space内部使用的内存块是通过内存映射匿名共享内存得到。

ImageSpace描述的是Image Space,DlMallocSpace【Doug Lea】描述的是Zygote Space和Allocation Space,LargeObjectMapSpace描述的是Large Object Space。它们都有一个共同的基类Space。

由于Image Space是不会进行新对象分配和垃圾回收的,因此它不像其它Space一样,还有另外一个Mark Bitmap。不过Space要求其子类要有一个Live Bitmap和一个Mark Bitmap,于是,ImageSpace就将内部的live_bitmap_同时作为Live Bitmap和Mark Bitmap来使用

  • Space - Large Object Space

ART运行时提供了两种Large Object Space实现。
1)其中一种实现和Continuous Space的实现类似,预先分配好一块大的内存空间,然后再在上面为对象分配内存块。不过这种方式实现的Large Object Space不像Continuous Space通过C库的内块管理接口来分配和释放内存,而是自己维护一个Free List。每次为对象分配内存时,都是从这个Free List找到合适的空闲的内存块来分配。释放内存的时候,也是将要释放的内存添加到该Free List去。

2)另外一种Large Object Space实现是每次为对象分配内存时,都单独为其映射一新的内存。也就是说,为每一个对象分配的内存块都是相互独立的。这种实现方式相比上面介绍的Free List实现方式,也更简单一些。【在Android 4.4中,ART运行时使用这种方式:LargeObjectMapSpace,它内部有一个成员变量large_objects_,里面保存的就是为每一个对象独立映射的内存块。】

  • Mod Union Table

除了Garbage Collector和Space,ART运行时垃圾收集机制比Dalvik垃圾收集机制还多了一个Mod Union Table的概念。Mod Union Table是与Card Table配合使用的,用来记录在一次GC过程中,记录不会被回收的Space的对象对会被回收的Space的引用。例如,Image Space的对象对Zygote Space和Allocation Space的对象的引用,以及Zygote Space的对象对Allocation Space的对象的引用。

  • Mod Union Table的处理过程

第一步:调用ModUnionTable类的成员函数ClearCards清理Card Table里面的Dirty Card,并且将这些Dirty Card记录在Mod Union Table中。
第二步:调用ModUnionTable类的成员函数Update遍历记录在Mod Union Table里面的Drity Card,并且找到对应的被修改对象,然后将被修改对象引用的其它对象记录起来。
第三步:调用ModUnionTable类的成员函数MarkReferences标记前面第二步那些被被修改对象引用的其它对象。

优势:使用Card Table可以在标记阶段重复使用,即在执行第二步之前,重复执行第一步,最后通过Mod Union Table将所有被被修改对象引用的其它对象收集起来统一进行标记,避免对相同对象进行重复标记。【Mod Union Table的作用就使得Card Table可以重复使用】

2. ART运行时堆的创建

注意变量heap_capacity的计算,它使用Zygote Space的结束地址减去Image Space的起始地址,得到的大小实际上是包含了boot.art@classes.oat文件映射到内存的大小。

只有地址空间连续的Space才具有Card Table。

创建用来记录在并行GC阶段,在Image Space上分配的对象对在Zygote Space和Allocation Space上分配的对象的引用的Mod Union Table,以及在Zygote Space上分配的对象对在Allocation Space上分配的对象的引用的Mod Union Table。前一个Mod Union Table使用ModUnionTableToZygoteAllocspace类来描述,后一个Mod Union Table使用ModUnionTableCardCache类来描述。

Zygote进程每次fork子进程之前,都执行一次垃圾收集,这样就可以使得fork出来的子进程有一个紧凑的堆空间。

对于新分配的对象,ART运行时不像Dalvik虚拟机一样,马上就将它们标记到对应的Space的Live Bitmap中去,而是将它们记录在Allocation Stack。这样做是为了可以执行Sticky Mark Sweep垃圾收集。

3. ART运行时的内存分配

ART运行时和Dalvik虚拟机为新创建对象分配内存的过程几乎是一模一样的,它们的区别仅仅是在于垃圾收集的方式和策略不同。

  • 内存碎片问题

在ART运行时中,主要用来分配对象的堆空间Zygote Space和Allocation Space的底层使用的都是匿名共享内存,并且通过C库提供的malloc和free接口来分进行管理。这样就可以通过dlmalloc技术来尽量解决碎片问题。

  • 只要满足以下三个条件,就在Large Object Space上分配,否则就在Zygote Space或者Allocation Space上分配

1) 请求分配的内存大于等于Heap类的成员变量large_object_threshold_指定的值。这个值等于3 * kPageSize,即3个页面的大小。
2)已经从Zygote Space划分出Allocation Space,即Heap类的成员变量have_zygote_space_的值等于true。
3)被分配的对象是一个原子类型数组,即byte数组、int数组和boolean数组等。

  • RecordAllocation

Heap类的成员函数RecordAllocation首先是记录当前已经分配的内存字节数以及对象数,接着再将新分配的对象压入到Heap类的成员变量allocation_stack_描述的Allocation Stack中去。后面这一点与Dalvik虚拟机的做法是不一样的,Dalvik虚拟机直接将新分配出来的对象记录在Live Bitmap中。ART运行时之所以要将新分配的对象压入到Allocation Stack中去,是为了以后可以执行Sticky GC

注意,如果不能成功将新分配的对角压入到Allocation Stack中,就说明:上次GC以来,新分配的对象太多了,因此这时候就需要执行一个Sticky GC,将Allocation Stack里面的垃圾进行回收,然后再尝试将新分配的对象压入到Allocation Stack中,直到成功为止。

  • 并行GC与非并行GC对内存不足的处理

在非并行GC运行模式中,在分配内存过程中遇到内存不足,并且当前可分配内存还未达到增长上限时,要等到执行完成一次非并行GC后,才能成功分配到内存,因为每次执行完成GC之后,都会按照预先设置的堆目标利用率来增长堆的大小。

在并行GC运行模式中,在分配内存过程中遇到内存不足,并且当前可分配内存还未达到增长上限时,不需要等到执行并行GC后,就有可能成功分配到内存,因为实际执行内存分配的Space可分配的最大内存字节数是足够的。

  • GC回收力度

kGcTypeSticky、kGcTypePartial和kGcTypeFull三种类型的GC的垃圾回收力度:
kGcTypeSticky只回收上次GC后在Allocation Space中新分配的垃圾对象;
kGcTypePartial只回收Allocation Space的垃圾对象;
kGcTypeFull同时回收Zygote Space和Allocation Space的垃圾对象。
通过这种策略,就有可能以最小代价解决分配对象时遇到的内在不足问题。不过,对于类型为kGcTypeSticky和kGcTypePartial的GC,它们的执行有前提条件的。

  • Sticky GC的触发条件

类型为kGcTypeSticky的GC的执行代码虽然是最小的,但是它能够回收的垃圾也是最小的。如果回收的垃圾不足于满足请求分配的内存,那就相当于做了一次无用功了。因此,执行类型为kGcTypeSticky的GC需要满足两个条件:
1)第一个条件是上次GC后在Allocation Space上分配的内存要达到一定的阀值,这样才有比较大的概率回收到较多的内存。
2)第二个条件Allocation Space剩余的未分配内存要达到一定的阀值,这样可以保证在回收得到较少内存时,也有比较大的概率满足请求分配的内存。

第一个阀值设置为2M,而上次GC以来分配的内存通过当前Allocation Space的大小估算得到,即通过调用Heap类的成员变量alloc_space_指向的一个DlMallocSpace对象的成员函数Size获得。
第二个阀值设置为1M,而Allocation Space剩余的未分配内存可以用Allocation Space的总大小减去当前Allocation Space的大小得到。通过调用Heap类的成员变量alloc_space_指向的一个DlMallocSpace对象的成员函数Capacity获得其总大小。

注意:类型为kGcTypePartial的GC的执行前提是已经从Zygote Space中划分出Allocation Space。

4. ART运行时的垃圾收集

ART运行时与Dalvik虚拟机一样,都使用了Mark-Sweep算法进行垃圾回收,因此它们的垃圾回收流程在总体上是一致的。但是ART运行时对堆的划分更加细致,因而在此基础上实现了更多样的回收策略。不同的策略有不同的回收力度,力度越大的回收策略,每次回收的内存就越多,并且它们都有各自的使用情景。这样就可以使得每次执行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结束阶段。
  • 并行GC和非并行GC的区别
  1. 非并行GC的标记阶段和回收阶段是在挂起所有的ART运行时线程的前提下进行的,因此只需要执行一次标记即可。

  2. 并行GC的标记阶段只锁住了Java堆,因此它不能阻止那些不是正在分配对象的ART运行时线程同时运行,而这些同时运行的ART运行时线程可能会引用了一些在之前的标记阶段没有被标记的对象。如果不对这些对象进行重新标记的话,那么就会导致它们被GC回收,造成错误。因此,与非并行GC相比,并行GC多了一个处理脏对象的阶段。所谓的脏对象就是我们前面说的在GC标记阶段同时运行的ART运行时线程访问或者修改过的对象。

注意:并行GC并不是自始至终都是并行的,例如:处理脏对象的阶段就是需要挂起除GC线程以外的其它ART运行时线程,这样才可以保证标记阶段可以结束

  • 五个守护线程

Heap类的成员函数RequestConcurrentGC调用Java层的java.lang.Daemons类的静态成员函数requestGC请求执行一次并行GC。Java层的java.lang.Daemons类在加载的时候,会启动五个与堆或者GC相关的守护线程,这五个守护线程分别是:

  1. ReferenceQueueDaemon:引用队列守护线程。我们知道,在创建引用对象的时候,可以关联一个队列。当被引用对象被GC回收的时候,被引用对象就会被加入到其创建时关联的队列去。这个加入队列的操作就是由ReferenceQueueDaemon守护线程来完成的。这样应用程序就可以知道哪些被引用对象引用的对象已经被回收了。

  2. FinalizerDaemon:析构守护线程。对于重写了成员函数finalize的对象,它们被GC决定回收时,并没有马上被回收,而是被放入到一个队列中,等待FinalizerDaemon守护线程去调用它们的成员函数finalize,然后再被回收。

  3. FinalizerWatchdogDaemon:析构监护守护线程。用来监控FinalizerDaemon线程的执行。一旦检测那些重写了成员函数finalize的对象在执行成员函数finalize时超出一定的时间,那么就会退出VM。

  4. HeapTrimmerDaemon:堆裁剪守护线程。用来执行裁剪堆的操作,也就是用来将那些空闲的堆内存归还给系统。

  5. GCDaemon:并行GC线程。用来执行并行GC。

  • kGcCauseForAlloc、kGcCauseBackground、kGcCauseExplicit

只要ART运行时当前不是处于正在关闭的状态,那么Heap类的成员函数ConcurrentGC就会检查当前是否正在执行GC。如果是的话,那么就等待它执行完成,然后再调用Heap类的成员函数CollectGarbageInternal触发一个原因为kGcCauseBackground的GC。否则的话,就直接调用Heap类的成员函数CollectGarbageInternal触发一个原因为kGcCauseBackground的GC。此外,还有第三种情况会触发GC,如果ART运行时支持显式GC,那么就它就会通过JNI调用Heap类的成员函数CollectGarbageInternal来触发一个原因为kGcCauseExplicit的GC。

无论是触发GC的原因是kGcCauseForAlloc、kGcCauseBackground或kGcCauseExplicit,他们最终都是通过调用Heap类的成员函数CollectGarbageInternal来执行GC的。

  • GC的标记过程

MarkSweep类的成员函数MarkingPhase标记对象的过程如下所示:

A. 调用成员函数BindBitmap设置回收范围。

B. 调用成员函数FindDefaultMarkBitmap找到回收策略为kGcRetentionPolicyAlwaysCollect的Space对应的Mark Bitmap,并且保存在成员变量current_mark_bitmap_中。

C. 调用Heap类的成员函数ProcessCards处理Card Table中的Dirty Card,以及这些Dirty Card添加到对应的Mod Union Table中去。

D. 调用Heap类的成员函数SwapStacks交换ART运行时的Allocation Stack和Live Stack。

E. 对于非并行GC,当前线程在挂起其它ART运行时线程的过程中,已经获得Locks类的静态成员变量mutator_lock_描述的读写锁的写访问,因此这时候就调用成员函数MarkRoots来标记那些不可以在没有获得Locks类的静态成员变量mutator_lock_描述的读写锁的情况下访问的根集对象。注意,MarkSweep类的成员函数MarkRoots只通过当前线程来标记根集对象。

F. 对于并行GC,由于标记阶段并没有挂起其它的ART运行时线程,因此这时候就调用成员函数MarkThreadRoots来并发标记那些不可以在没有获得Locks类的静态成员变量mutator_lock_描述的读写锁的情况下访问的位于【线程】调用栈中的根集对象,接着再在当前线程中调用成员函数MarkNonThreadRoots标记那些不可以在没有获得Locks类的静态成员变量mutator_lock_描述的读写锁的情况下访问的【其它】根集对象。

G. 获得Live Stack的大小,保存成员变量live_stack_freeze_size_中。注意,这时候Live Stack的大小即为交换Allocation Stack和Live Stack之前Allocation Stack的大小,即从上次GC以来新分配的对象的个数。

H. 调用成员函数MarkConcurrentRoots标记那些可以在没有获得Locks类的静态成员变量mutator_lock_描述的读写锁的情况下访问的根集对象。

I. 调用Heap类的成员函数UpdateAndMarkModUnion处理Mod Union Table中的Dirty Card。

J. 调用成员函数MarkReachableObjects递归标记那些可以从根集对象到达的其它对象。

  • Dirty Card

Dirty Card记录的是在不需要进行回收的Space上分配的并且在GC过程中类型为引用的成员变量被修改过的对象,这些被修改的引用类型的成员变量有可能指向了一个在需要进行回收的Space上分配的对象,而这些对象可能不在根集中,因此就需要将它们当作根集对象一样进行标记。

在Dalvik虚拟机的垃圾收集过程中,Dirty Card是在Handle Dirty Object阶段才处理的,那为什么ART运行时会放在Marking阶段就进行处理呢?实际上,ART运行时在Handle Dirty阶段也会对Dirty Card进行处理。也就是说,在ART运行时的垃圾收集过程中,Dirty Card一共被处理两次,一次在Marking阶段,另一次在Handle Dirty Object阶段。这样做有两个原因:

1)第一个原因是在ART运行时中,Card Table在并行和非并行GC中都会用到,因此就不能只在Handle Dirty Object阶段才处理。ART运行时中的Card Table在运行期间一直都是用来记录那些修改了引用类型的成员变量的对象的,即这些对象对应的Card都会设置为DIRTY。这些Dirty Card在每次GC时都会进行老化处理,老化处理是通过一个叫AgeCardVisitor的类进行的。每一个Card只有三个可能的值,分别是DIRTY、(DIRTY - 1)和0。当一个对象的引用类型的成员变量被修改之后,它对应的Card的值就会被设置为DIRTY。在接下来的一次GC中,值为DIRTY的Card就会老化为(DIRTY - 1)。在又接下来的GC中,值为(DIRTY-1)的Card就会老化为0。【对于值等于DIRTY和(DIRTY - 1)的Card对应的对象,它们在Marking阶段都会被标记】。这些对象被标记就意味着它们不会被回收,即使它们不在根集中,并且也不被根集对象引用。尽管这样会造成一些垃圾对象没有及时回收,不过这不会引起程序错误,并且这些对象在再接下来的一次GC中,如果仍然不在根集中,或者不被根集对象引用,那么它们就一定会被回收,因为它们对应的Card已经老化为0了。

2)第二个原因是与并行GC相关的。我们知道,Handle Dirty Object阶段是在挂起ART运行时线程的前提下进行的,因此,如果把所有的Dirty Card都放在Handle Dirty Object阶段处理,那么就会可能会造成应用程序停顿时间过长。于是,ART运行时就在并行Marking阶段也帮忙着处理Dirty Card,通过这种方式尽量减少在Handle Dirty Object阶段需要处理的Dirty Card,以达到减少应用程序因为GC造成的停顿时间。

不过这样就会有一个问题:在Handle Dirty Object阶段,如何知道哪些Dirty Card是在并行Marking阶段已经被处理过的呢?这就要借助Mod Union Table。在Marking阶段,当一个Dirty Card被处理过后,它的值就会由DIRTY变成(DIRTY - 1),并且被它引用的对象都会被记录在Mod Union Table中。这样我们就可以在Marking阶段和Handle Dirty Object阶段做到共用同一个Card Table,而且又能够区分不同的阶段出现的Dirty Card。

  • Dirty Card的处理

Heap类的成员变量image_mod_union_table_指向的是一个ModUnionTableToZygoteAllocspace对象,用来记录在Image Space上分配的对象对在Zygote Space和Allocation Space上分配的对象的引用,另外一个成员变量zygote_mod_union_table_则指向一个ModUnionTableCardCache,用来记录在Zygote Space上分配的对象对在Allocation Space上分配的对象的引用。

对于与Image Space和Zygote Space对应的Dirty Card的处理,是分别通过调用ModUnionTableToZygoteAllocspace类和ModUnionTableCardCache类的成员函数ClearCards来进行的。

对于Allocation Space,它没有对应的Mod Union Table,因此,与它对应的Dirty Card的处理,就直接在Card Table进行处理,即调用CardTable类的成员函数ModifyCardsAtomic来进行。

对于Partial Mark Sweep,需要处理的Card Table包括Image Space和Zygote Space对应的Card Table,而对于Full Mark Sweep,需要处理的Card Table只有Image Space对应的Card Table。

  • MarkThreadRoots

对于位于线程调用栈的根集对象的标记,可以做利用CPU的多核特性做一些并发优化。注意,这时候除了当前执行GC的线程可以运行之外,其它的ART运行时线程也是可以运行的。这样就可以让正在运行的ART运行时线程来并发标记各自那些位于调用栈上的根集对象。这个工作是通过调用MarkSweep类的成员函数MarkThreadRoots来实现的。

  • MarkReachableObjects

递归标记那些根集对象引用的其它对象,这是通过调用MarkSweep类的成员函数MarkReachableObjects来实现的。MarkSweep类的成员函数MarkReachableObjects首先会将Live Stack的对象全部进行标记。这里的Live Stack其实就是之前的Allocation Stack,也就是说,Mark Sweep不会对上次GC以后分配的对象进行垃圾回收。Partial Mark Sweep也是通过父类MarkSweep类的成员函数MarkReachableObjects来递归标记根集对象引用的对象的,也就是说,Mark Sweep也不会对上次GC以后分配的对象进行垃圾回收。由于Sticky Mark Sweep刚好相反,它要对上次GC以后分配的对象进行垃圾回收,因此,它就必须要重写MarkSweep类的成员函数MarkReachableObjects。

  • MarkSweep类的成员函数HandleDirtyObjectsPhase主要操作

A. 调用成员函数ReMarkRoots重新标记根集对象。

B. 调用成员函数RecursiveMarkDirtyObjects递归标记值等于Card Table中值等于DIRTY的Card。注意,此时值等于(DIRTY - 1)的Card已经在标记阶段处理过了,因此这里不需要再对它们进行处理。

C. 调用成员函数ProcessReferences处理Soft Reference、Weak Reference、Phantom Reference和Finalizer Reference等引用对象。

D. 调用Runtime类的成员函数DisallowNewSystemWeaks禁止在系统内部分配全局弱引用对象、Monitor对象和常量字符池对象等,因此这些新分配的对象会得不到标记,从而会导致在接下来的清除阶段中被回收,但是它们又是正在使用的。

  • ReclaimPhase

MarkSweep类的成员函数ReclaimPhase首先判断当前执行的GC是并行还是非并行的。对于并行GC,在前面的Handle Dirty Object阶段,已经对引用对象作过处理了。但是对于非并行GC,由于不需要执行Handle Dirty Object阶段,因此这时就要调用MarkSweep类的成员函数ProcessReferences对Soft Reference、Weak Reference、Phantom Reference和Finalizer Reference等引用对象进行处理。

MarkSweep类的成员函数ReclaimPhase接下来再调用另外一个成员函数SweepSystemWeaks清理那些没有被标记的常量字符串、Monitor对象和在JNI创建的全局弱引用对象。

MarkSweep类的成员函数Sweep首先回收Contiouous Space的垃圾,再回收Discontinous Space的垃圾,也就是Large Object Space的垃圾。这里首先要注意的一点是,只有Mark Sweep和Partial Mark Sweep会调用MarkSweep类的成员函数Sweep来清除垃圾【会清理Large Object Space】,Sticky Mark Sweep会重写父类Mark Sweep的成员函数Sweep,因为它需要清理的只是Allocation Stack的垃圾。

MarkSweep类的成员函数SweepLargeObjects通过遍历Large Object Space的每一个对象,并且检查它们在Mark Bitmap的标记位是否被设置了。如果没有被设置,那么就调用Large Object Space的成员函数Free对其占用的内存块进行释放。垃圾清理完毕,MarkSweep的成员函数SweepLargeObjects也会更新当前释放的对象计数和内存计数。

  • SweepArray

保存在Allocation Stack中的对象要么是在Allocation Space上分配的,要么是Large Object Space分配的。

MarkSweep类的成员函数SweepArray一开始就先取出这两个Space的Mark Bitmap,然后再遍历Allocation Stack的对象。对于Allocation Stack中每一个对象,首先判断它在Allocation Space的Mark Bitmap中的标记位是否被设置了。如果没有设置了,那么就说明这是一个可能需要回收的对象。如果在Allocation Space的Mark Bitmap中的标记位没有被设置,再判断它在Large Object Space的Mark Bitmap中的标记位是否被设置了。如果没有被设置,那么也说明这是一个需要回收的对象。

对于属于Allocation Space的垃圾对象,MarkSweep类的成员函数SweepArray通过调用DlMallocSpace类的成员函数FreeList来批量回收。而对于属于Large Object Space的垃圾对象,MarkSweep类的成员函数SweepArray则是通过LargeObjectMapSpace类的成员函数Free来逐个回收。

  • FinishPhase

MarkSweep类的成员函数FinishPhase负责执行一些善后工作,包括:

A. 调用Heap类的成员函数EnqueueClearedReferences将目标对象已经被回收了的引用对象添加到各自关联的队列中去,以便应用程序可以知道它们的目标对象已经被回收。

B. 调用Heap类的成员函数GrowForUtilization根据预先设置的堆目标利率以及最小和最大空闲内存数增长堆的大小。

C. 调用Heap类的成员函数RequestHeapTrim对堆进行裁剪,以便可以将空闲内存临时归还给操作系统。

D. 更新GC执行时间、暂停时间、释放的对象个数和内存字节数等统计数据。

E. 清空所有Space的Mark Bitmap和ART运行时的Mark Stack。

5. 总结

与Dalvik虚拟机GC相比,ART运行时GC的优势在于:

  1. ART运行时堆的划分和管理更细致,它分为Image Space、Zygote Space、Allocation Space和Large Object Space四个Space,再加上一个Allocation Stack。其中,Allocation Space和Large Object Space和Dalvik虚拟机的Zygote堆和Active堆作用是一样的,而其余的Space则有特别的作用,例如:Image Space的对象是永远不需要回收的。

  2. ART运行时的每一个Space都有不同的回收策略,ART运行时根据这个特性提供了Mark Sweep、Partial Mark Sweep和Sticky Mark Sweep等三种回收力度不同的垃圾收集器。其中,Mark Sweep的垃圾回收力度最大,它会同时回收Zygote Space、Allocation Space和Large Object Space的垃圾,Partial Mark Sweep的垃圾回收力度居中,它只会同时回收Allocation Space和Large Object Space的垃圾,而Sticky Mark Sweep的垃圾回收力度最小,它只会回收Allocation Stack的垃圾,即上次GC以后分配出来的又不再使用了的对象。力度越大的垃圾收集器,回收垃圾时需要的时候也就越长。这样我们就可以在应用程序运行的过程中根据不同的情景使用不同的垃圾收集器,那就可以更有效地执行垃圾回收过程。

  3. ART运行时充分地利用了设备的CPU多核特性,在并行GC的执行过程中,将每一个并发阶段的工作划分成多个子任务,然后提交给一个线程池执行,这样就可以更高效率地完成整个GC过程,避免长时间对应用程序造成停顿。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344