Android GC 从dalvik到ART的改进分析

自从android4.4开始加入ART虚拟机,从5.0开始完全用ART代替Dalvik虚拟机,ART的优点越来越被开发者和用户接受。很多用户会觉得android从5.0开始很流畅,很少卡顿,其实从GC角度看,是因为系统GC的过程得到了优化,GC过程中的几次暂停时间缩短了。接下来我主要从dalvik和ART二者的GC的改进的地方来分析一下,为什么ART的使用会使android系统性能和流畅有着如此之大的提升。
我将分别从Dalvik和ART两方面来介绍相关的详细机制和实现,然后再对比二者的改进,结合实验数据来探讨android系统在ART的GC的改进优势。

1. Dalvik的垃圾收集

在Dalvik时代,Android更是在这方面深受困扰。由于内存较小,每次APP应用程序需要分配内存时,如果堆空间不能提供一个足够大的空间时就会启动Dalvik的垃圾收集器。GC收集器会遍历整个堆地址空间,然后查看每个app分配的对象,并进行相应的标记,然后进行回收过程。
Dalvik除了要给新的应用程序分配内存空间外,还需要对已经分配的内存空间的对象进行内存管理,主要就是我们之前提到的自动垃圾收集。这个垃圾收集主要是mark sweep算法实现的。简单点说,dalvik虚拟机的mark-sweep算法就分为两个阶段,即mark阶段和sweep阶段。Dalvik的GC过程,可以大致归纳为如下过程:
整个标记开始之前,首先遍历一次堆地址空间。
Mark阶段: 从对象的根集对象开始标记引用对象。
Sweep阶段: 回收没有被标记的对象占用的内存。

Mark阶段就是从根集对象开始标记被引用的对象,在完成了mark标记后,就进入sweep阶段,即回收那些没有被标记的对象占用的内存。接下来我们主要分析一下这个算法,才能更好的比较分析新的ART和dalvik的区别。其实sweep阶段很简单了,现在的重点是讲解一下mark阶段。

1.1 dalvik的mark阶段

Mark阶段最主要的工作就是标记已经被引用的对象。用到的最主要的一个数据结构叫heap bitmap。事实上,总共使用了两个bitmap,一个是live bitmap,一个是mark bitmap。这个bitmap里的每一位对应一个对象,某个对象被引用了,就标1,没引用就标0。Livebitmap主要用来标记上一次GC时被引用的对象,也就是那些没有被回收的对象,而markbitmap主要用来标记当前GC有被引用的对象。因此在判断需要回收哪些对象时,就是那些在Live bitmap中标为1,而在mark bitmap中标为0的对象。
仔细思考这个流程可以发现,mark bitmap实际上就是live bitmap的一个子集。
在mark阶段,要求除了GC线程外,其他的线程都需要停止,否则就可能不能正确的标记每个对象,因为可能刚标记完又被修改引用等等情况的发生。这种现象叫stop the world,会导致该应用程序中止执行,造成停顿,为了减少这种停顿的发生,dalvik采用了并行的垃圾回收算法,即Concurrent GC。
在整个mark开始时,GC会先不得不中止一次程序的运行,从而对堆地址空间进行一次遍历,这次遍历可以获得每一个应用程序分配的对象,就能确认每个对象在内存堆中的大小、起始地址等等信息。 这个停顿在dalvik里是不得不做的事情,每次GC都会必须触发一次堆地址空间的遍历引起的停顿。
接下来就是真正的标记阶段了。为了减少stop the world带来的负面影响,dalvik的GC采用了Concurrent方法,实现时,GC将mark阶段又分为了两个子阶段,一个是标记根集对象,一个是标记根集对象引用的对象。所谓根集对象,就是指在GC线程开始的时候,那些被全局变量、栈变量和寄存器等引用的对象。通过这些根集变量,可以顺着它们找到其余的被引用变量。比如说,假如发现了一个栈变量引用一个对象,而这个对象又引用了另外一个对象,那这个被引用的对象也会被标记为正在使用。这个标记“被根集对象引用的对象”的过程就是第二个子阶段。在ConcurrentGC中,mark的第一个阶段,在标记根集对象的阶段是不允许GC线程之外的线程运行的,但是mark的第二个阶段允许其他线程运行。这样就可能带来一个问题是,在第二个阶段执行的过程中,如果某个运行的线程修改了一个对象了内容,由于很有可能引用了新的对象,所以这个对象也必须要记录起来。否则会发生被引用对象还在使用却被回收。这种情况出现在只进行部分GC的情况,这时候Card Table的作用就是用来记录非GC堆对象对GC的堆对象的引用。Dalvik的堆空间,分为zygote heap 和 active heap。 前者主要存放一些在zygote时就分配的对象,后者主要用于之后新分配的空间,Dalvik虚拟机进行部分垃圾收集时,实际上就是只收集在Active heap上分配的对象。Card Table就是用来记录在Zygote heap上分配的对象在GC执行过程中对在Active heap上分配的对象的引用。
Card table由一系列card组成,一个card实际上就是一个字节,它的值分为clean和dirty两种。如果一个Card的值是CLEAN,就表示与它对应的对象在Mark第二个子阶段没有被程序修改过。如果一个Card的值是DIRTY,就意味着被程序修改过,对于这些被修改过的对象。需要在Mark第二子阶段结束之后,再次禁止GC线程之外的其它线程执行,以便GC线程再次根据Card Table记录的信息对被修改过的对象引用的其它对象进行重新标记。由于Mark第二子阶段执行的时间不会太长,因此在这个阶段被修改的对象不会很多,这样就可以保证第二次子阶段结束后,再次执行标记对象的过程是很快的,因而此时对程序造成的停顿非常小。
在mark阶段,主要是标记的第二个子阶段,dalvik是采用递归的方式来遍历标记对象。但是这个递归并不是像一般的递归一样借助一个递归函数来实现,而是使用一个叫做mark stack的栈空间实现。大致过程是:一个被引用的对象在标记的过程中,先被标记,然后放在栈中,等该对象的父对象全部被标记完成后,依次弹出栈中的每一个对象,并标记其引用,然后把其引用再丢到栈中。
这里可能会有一个疑问,一般在递归时都采用的函数递归的方法,但是为什么这里要采用mark stack呢?可以思考,采用mark stack栈而不是函数递归的好处是:首先可以采用并行的方式来做,将栈进行分段,多个线程共同将栈中的数据递归标记。其次,可以避免函数堆栈占用较深。
至此,差不多介绍了dalvik的GC的mark阶段的过程。我们可以发现,在mark阶段,一共有3次停顿,一次是在标记开始前遍历堆地址空间的停顿,一次是在标记的第一个阶段标记所有根集对象时的停顿,还有一次是在标记的第二个子阶段完成后处理card table时的停顿。这3次停顿的时间直接影响了android上应用程序的表现,尤其是卡顿现象,因此ART在这块有重点改进,下文会介绍ART上的过程。

1.2 dalvik的sweep阶段

其实sweep阶段就很简单了,在mark阶段已经提到过,GC时回收的是在live bitmap里标为1而在mark bitmap里标为0的对象。而这个mark bitmap实际上就是live bitmap的子集,因此在sweep阶段只需要处理二者的差集即可,在回收掉相应的对象后,只需要再把live bitmap和mark bitmap的指针调换一下,即这次的mark bitmap作为下一次GC时的live bitmap,然后清空live bitmap。
其过程和ART的没什么太大变化,而由于在android 5.0源码中已经去掉了dalvik,这环节的具体解释就在ART部分分析,但是实际上在sweep阶段dalvik和ART二者没有太大区别,因为主要只是处理相应的bitmap的对应的对象的内存,ART也没有什么优化的地方。

2 ART的垃圾收集

ART同样采用了自动GC的策略,并且同样不可避免的使用到了经典的mark-sweep算法。

2.1 mark-sweep收集器

在android源码中,ART的部分的GC在使用mark-sweep算法进行自动垃圾收集时,根据轻重程度不同,分为三类,sticky,partial,full。可以看到,ART里的GC的改进,首先就是收集器的多样化。
而根据GC时是否暂停所有的线程分类并行和非并行两类。所以在ART中一共定义了6个mark-sweep收集器。参看art/runtime/gc/heap.cc可见。根据不同情况,ART会选择不同的GC collector进行GC工作。其实最复杂的就是Concurrent Mark Sweep 收集器。如果理解了最复杂的Concurrent Mark Sweep算法,其他5种GC收集器的工作原理就也理解了。同样的,垃圾回收工作从整体上可以划分两个大的阶段:Mark 和 Sweep。

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里的第二次停顿的带来的卡顿时间。这个改进非常重要。
在对mark stack使用时,在初始阶段,为后面的mark准备好markstack
但是值得一提的是,在标记开始阶段,有别于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,即是可以回收的垃圾了。
经过了前面3次扫描以及Mark,我们的mark bitmap已经很完整了。但是值得注意的是,由于Sweep的操作是对应于live bitmap,即那些在live bitmap中标记过,却在mark bitmap中没有标记的对象。也就是说,mark bitmap中标记的对象是live bitmap中标记对象的子集。但目前为止live bitmap标记的对象还不是最全,因为前文有提到过,为了消除dalvik的第一次停顿,ART计入了allocation stack中的对象,还没有标记。Allocation stack先“搁置”起来不让后面的主线程使用,启用备份的的live stack。

1
2
3
void Heap::SwapStacks() {
allocation_stack_.swap(live_stack_);
}

2) Sweep阶段

在完成了mark阶段后,对应已经标好的live bitmap和mark bitmap,需要进入sweep来回收相应的垃圾。Sweep阶段就是把那些二者的差集所占用的内存回收掉。

3) Finish阶段

在dalvik中没有发现的是,ART中可以归纳为有一个第三个阶段,就是类似的一个finish阶段。

1
2
3
4
5
6
7
8
9
void MarkSweep::FinishPhase() {
base::TimingLogger::ScopedSplit split("FinishPhase", &timings_);
// Can't enqueue references if we hold the mutator lock.
Object* cleared_references = GetClearedReferences();
Heap* heap = GetHeap();
timings_.NewSplit("EnqueueClearedReferences");
heap->EnqueueClearedReferences(&cleared_references);
......
}

因为之前说过mark bitmap是live bitmap的一个子集,而mark bitmap中包含了所有的正在被引用的的非垃圾的对象,因此需要交换mark bitmap和live bitmap的指针,使mark bitmap作为下一次GC的live bitmap,并且重置新的mark bitmap。

1
2
3
4
5
6
7
//Clear all of the spaces' mark bitmaps.
for (const auto& space : GetHeap()->GetContinuousSpaces()) {
if (space->GetGcRetentionPolicy() != space::kGcRetentionPolicyNeverCollect) {
space->GetMarkBitmap()->Clear();
}
}
mark_stack_->Reset();

另外,需要指出的是,由于mark stack的目的是为了方便标记的递归,所以在Finish阶段,也需要把mark stack给清空,至于实现可以看以上代码行。

2.2 sticky mark sweep收集器

其实sticky mark sweep的主要步骤也是和mark sweep的过程大致一样,主要完成三次并发的mark阶段,然后进行一个stop the world的非并发进行一次对堆对象的遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
void StickyMarkSweep::BindBitmaps() {
PartialMarkSweep::BindBitmaps();
WriterMutexLock mu(Thread::Current(), *Locks::heap_bitmap_lock_);
// For sticky GC, we want to bind the bitmaps of all spaces as the allocation stack lets us
// know what was allocated since the last GC. A side-effect of binding the allocation space mark
// and live bitmap is that marking the objects will place them in the live bitmap.
for (const auto& space : GetHeap()->GetContinuousSpaces()) {
if (space->GetGcRetentionPolicy() == space::kGcRetentionPolicyAlwaysCollect) {
BindLiveToMarkBitmap(space);
}
}
GetHeap()->GetLargeObjectsSpace()->CopyLiveToMarked();
}

但是可以通过实现方法发现,有一个getGcRetenionPolicy,获取的是一个枚举。

只有符合总是收集的条件的,就把live bitmap和mark bitmap绑定起来。其余的过程和full是一样的。Sticky Mark sweep只扫描自从上次GC后被修改过的堆空间,并且也只回收自从上次GC后分配的空间。Sticky是只回收kGcRetentionPolicyAlwaysCollect的space。不回收其他两个,因此sticky的回收的力度是最小的。作为最全面的full mark sweep, 上面的三个策略都是会回收的。

2.3 partial mark sweep收集器

这是mark sweep收集器里使用的最少的GC收集策略。按照官方文档,一般是使用sticky mark sweep较多。这里有一个概念就是吞吐率,即一次GC释放的字节数和GC持续的时间(秒)的比值。由于一般是sticky mark sweep进行GC,所以当上次GC的吞吐率小于同时的partial mark sweep的吞吐率时,就会把下次GC收集器从sticky变成partial。但是在partial执行一次GC后,就仍然会恢复到stick mark sweep收集器。
阅读源码发现,partial重写了父类的成员函数。
其实分析这些可以发现,从full mark sweep到partial mark sweep到stick mark sweep,GC的力度是越来越小的,因为可以回收的越来越少。之所以说回收力度大,就是指可以回收的space多,比如上图的partial, 是不回收kGcRetentionPolicyFullCollect,但是是会回收kGcRetentionPolicyAlwaysCollect的space的。
因此partial mark sweep每次执行一次GC后,就会自动切换到sticky策略,这样才能使系统更流畅得进行GC,并减少了GC带来的消耗。。

2.4 其他区别

其实观察space目录的文件可以发现,有一个新的概念就是large object space。事实上,ART还引入了一个新的的方法就是大对象存储空间(large object space,LOS),这个空间与堆是相互独立的,但是仍然是驻留在应用程序的内存空间中。方便让ART可以更好的管理较大的对象,比如android里的bitmap。在dalvik中,在对堆空间进行分段时,占用空间较大的对象会带来一些问题。例如,在分配一个bitmap大对象时,由于占用空间较大,可能引起GC的启动次数也会增加,从而增加了开销。有了LOS,GC收集器因堆空间分段而引发调用次数将会大大降低,这样垃圾收集器就能做更加合理的内存分配,从而降低运行时开销。

3 实验对比证明

为了更形象的了解ART在GC这个环节对比Dalvik到底有了多大的改善,我同样的进行的实验。实验使用的手机,保证了总的RAM内存是相同的,安装的应用程序也都是相同的。我们分别在dalvik模式下和ART模式下观察运行支付宝APP的GC表现。

在ART下启动运行开始时的GC表现。

03-04 10:07:20.524: I/art(5703): Background partial concurrent mark sweep GC freed 65419(7MB) AllocSpace objects, 136(11MB) LOS objects, 20% free, 60MB/76MB, paused 6.695ms total 107.116ms
03-04 10:08:05.302: I/art(2176): Background partial concurrent mark sweep GC freed 63122(4MB) AllocSpace objects, 2(40KB) LOS objects, 24% free, 50MB/66MB, paused 1.289ms total 123.983ms
我从GC 的log中截取了上述一段。其中的显式(GC_EXPLICIT)和并发(GC_CONCURRENT)是GC中比较通用的清除垃圾的调用。GC_FOR_ALLOC则是在内存分配器尝试分配新的内存空间,但堆空间不够用时调用的。可以看到,在dalvik模式下刚启动支付宝的几秒内,触发了28次GC事件,总共停顿耗时4657ms。而在ART模式下,可以看到一共触发了2次GC事件,共耗时231.099ms。
我们还可以拿之前的一次测试来看看结果,这次测试是当时操作系统课堂报告时做的测试,测试观察百度地图这个应用程序的GC表现,结果可以从下面看出,分别是dalvik和ART的log。

12-24 15:34:21.046: D/dalvikvm(577): GC_FOR_ALLOC freed 3690K, 19% free 30176K/36844K, paused 517ms, total 517ms
12-24 15:34:21.062: D/dalvikvm(30509): GC_CONCURRENT freed 1968K, 49% free 11561K/22312K, paused 28ms+5ms, total 224ms
12-24 15:34:21.062: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 179ms
12-24 15:34:21.070: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 183ms
12-24 15:34:21.078: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 188ms
12-24 15:34:21.093: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 124ms
12-24 15:34:21.523: D/dalvikvm(577): GC_FOR_ALLOC freed 6K, 17% free 33769K/40448K, paused 452ms, total 452ms
12-24 15:34:21.585: D/dalvikvm(30509): GC_CONCURRENT freed 1005K, 49% free 11554K/22312K, paused 30ms+19ms, total 261ms
12-24 15:34:21.585: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 192ms
12-24 15:34:21.585: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 199ms
12-24 15:34:22.171: D/dalvikvm(577): GC_CONCURRENT freed 110K, 17% free 33744K/40448K, paused 99ms+52ms, total 612ms
12-24 15:34:22.171: D/dalvikvm(577): WAIT_FOR_CONCURRENT_GC blocked 336ms
12-24 15:34:22.421: D/dalvikvm(877): GC_CONCURRENT freed 1203K, 62% free 7760K/20000K, paused 147ms+20ms, total 354ms
12-24 15:34:22.437: D/dalvikvm(577): GC_FOR_ALLOC freed 68K, 16% free 35701K/42476K, paused 255ms, total 255ms
12-24 15:34:23.101: D/dalvikvm(577): GC_CONCURRENT freed 661K, 16% free 35720K/42476K, paused 23ms+83ms, total 668ms
12-24 15:34:24.632: D/dalvikvm(758): GC_CONCURRENT freed 387K, 33% free 10427K/15496K, paused 4ms+17ms, total 135ms
12-24 15:34:24.640: D/dalvikvm(30509): GC_CONCURRENT freed 691K, 48% free 11804K/22312K, paused 5ms+33ms, total 279ms
12-24 15:34:24.906: D/dalvikvm(30825): GC_CONCURRENT freed 305K, 15% free 8710K/10160K, paused 3ms+2ms, total 63ms
12-24 15:34:26.179: D/dalvikvm(30509): GC_CONCURRENT freed 875K, 47% free 11998K/22312K, paused 147ms+6ms, total 498ms
12-24 15:34:26.179: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 362ms
12-24 15:34:26.453: D/dalvikvm(30509): GC_CONCURRENT freed 696K, 45% free 12430K/22312K, paused 21ms+27ms, total 229ms
12-24 15:34:26.992: D/dalvikvm(30509): GC_CONCURRENT freed 1626K, 47% free 12018K/22312K, paused 8ms+5ms, total 62ms
12-24 15:34:27.062: D/dalvikvm(30509): GC_CONCURRENT freed 1038K, 47% free 12016K/22312K, paused 5ms+5ms, total 53ms
12-24 15:34:27.140: D/dalvikvm(30509): GC_CONCURRENT freed 1051K, 47% free 12016K/22312K, paused 7ms+5ms, total 57ms
12-24 15:34:27.257: D/dalvikvm(30509): GC_CONCURRENT freed 1038K, 47% free 12017K/22312K, paused 5ms+6ms, total 101ms
12-24 15:34:27.429: D/dalvikvm(30509): GC_CONCURRENT freed 1050K, 47% free 12017K/22312K, paused 5ms+6ms, total 55ms
12-24 15:34:27.531: D/dalvikvm(30509): GC_CONCURRENT freed 1043K, 47% free 12016K/22312K, paused 5ms+5ms, total 57ms
12-24 15:34:27.601: D/dalvikvm(30509): GC_CONCURRENT freed 1043K, 47% free 12016K/22312K, paused 5ms+6ms, total 57ms
12-24 15:34:27.710: D/dalvikvm(30509): GC_CONCURRENT freed 1037K, 47% free 12016K/22312K, paused 5ms+7ms, total 78ms
12-24 15:34:27.796: D/dalvikvm(30509): GC_CONCURRENT freed 1042K, 47% free 12016K/22312K, paused 5ms+6ms, total 58ms
12-24 15:34:27.882: D/dalvikvm(30509): GC_CONCURRENT freed 1036K, 47% free 12016K/22312K, paused 5ms+5ms, total 51ms
12-24 15:34:27.960: D/dalvikvm(30509): GC_CONCURRENT freed 1041K, 47% free 12016K/22312K, paused 5ms+5ms, total 53ms
12-24 15:34:28.039: D/dalvikvm(30509): GC_CONCURRENT freed 1041K, 47% free 12016K/22312K, paused 5ms+5ms, total 56ms
12-24 15:34:28.414: D/dalvikvm(30509): GC_CONCURRENT freed 1153K, 47% free 11987K/22312K, paused 9ms+6ms, total 137ms
12-24 15:34:28.414: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 106ms
12-24 15:34:28.468: D/dalvikvm(30509): GC_CONCURRENT freed 766K, 45% free 12293K/22312K, paused 7ms+6ms, total 44ms
12-24 15:34:28.468: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 37ms
12-24 15:34:28.523: D/dalvikvm(30509): GC_FOR_ALLOC freed 1063K, 45% free 12421K/22312K, paused 46ms, total 46ms
12-24 15:34:28.921: D/dalvikvm(30509): GC_CONCURRENT freed 1044K, 44% free 12548K/22312K, paused 11ms+8ms, total 174ms
12-24 15:34:31.210: D/dalvikvm(12328): GC_FOR_ALLOC freed 345K, 15% free 10241K/12048K, paused 109ms, total 109ms
12-24 15:34:31.359: D/dalvikvm(12328): GC_FOR_ALLOC freed <1K, 16% free 10249K/12060K, paused 153ms, total 153ms
12-24 15:34:31.437: D/dalvikvm(12328): GC_FOR_ALLOC freed 11K, 16% free 10237K/12060K, paused 76ms, total 76ms
12-24 15:34:31.445: D/dalvikvm(30509): GC_CONCURRENT freed 549K, 41% free 13325K/22312K, paused 14ms+20ms, total 196ms
12-24 15:34:31.445: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 184ms
12-24 15:34:31.445: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 164ms
12-24 15:34:31.445: D/dalvikvm(30509): WAIT_FOR_CONCURRENT_GC blocked 88ms
12-24 15:34:31.507: D/dalvikvm(12328): GC_FOR_ALLOC freed 0K, 16% free 10245K/12072K, paused 66ms, total 66ms
12-24 15:34:31.570: D/dalvikvm(30509): GC_FOR_ALLOC freed 2347K, 45% free 12289K/22312K, paused 61ms, total 61ms
12-24 15:34:31.640: D/dalvikvm(30509): GC_FOR_ALLOC freed 689K, 45% free 12414K/22312K, paused 44ms, total 44ms
12-24 15:34:32.101: D/dalvikvm(12328): GC_FOR_ALLOC freed 23K, 16% free 10240K/12072K, paused 36ms, total 36ms
12-24 15:34:32.140: D/dalvikvm(12328): GC_FOR_ALLOC freed <1K, 16% free 10247K/12084K, paused 37ms, total 37ms
12-24 15:34:32.210: D/dalvikvm(12328): GC_FOR_ALLOC freed <1K, 16% free 10247K/12084K, paused 71ms, total 71ms
12-24 15:34:32.398: D/dalvikvm(12328): GC_FOR_ALLOC freed 0K, 16% free 10255K/12096K, paused 188ms, total 188ms
12-24 15:34:32.515: D/dalvikvm(12328): GC_FOR_ALLOC freed 31K, 16% free 10239K/12096K, paused 45ms, total 45ms
12-24 15:34:32.578: D/dalvikvm(12328): GC_FOR_ALLOC freed 32K, 16% free 10239K/12096K, paused 30ms, total 30ms
12-24 15:34:32.695: D/dalvikvm(12328): GC_FOR_ALLOC freed 79K, 16% free 10247K/12096K, paused 49ms, total 49ms
12-24 15:34:35.054: D/dalvikvm(30509): GC_CONCURRENT freed 1760K, 47% free 11834K/22312K, paused 5ms+13ms, total 106ms
12-24 15:34:38.265: D/dalvikvm(30509): GC_FOR_ALLOC freed 606K, 46% free 12058K/22312K, paused 55ms, total 56ms
12-24 15:34:38.351: D/dalvikvm(30509): GC_FOR_ALLOC freed 254K, 45% free 12308K/22312K, paused 68ms, total 70ms
12-24 15:34:38.429: D/dalvikvm(30509): GC_FOR_ALLOC freed 521K, 43% free 12803K/22312K, paused 64ms, total 64ms
12-24 15:34:38.500: D/dalvikvm(30509): GC_FOR_ALLOC freed 1952K, 46% free 12112K/22312K, paused 46ms, total 46ms
12-24 15:34:38.554: D/dalvikvm(30509): GC_FOR_ALLOC freed 276K, 45% free 12338K/22312K, paused 44ms, total 44ms
12-24 15:34:38.609: D/dalvikvm(30509): GC_FOR_ALLOC freed 514K, 43% free 12833K/22312K, paused 44ms, total 44ms
12-24 15:34:38.664: D/dalvikvm(30010): GC_CONCURRENT freed 402K, 15% free 8869K/10416K, paused 3ms+2ms, total 31ms
12-24 15:34:38.671: D/dalvikvm(30509): GC_FOR_ALLOC freed 1193K, 43% free 12796K/22312K, paused 51ms, total 52ms
12-24 15:34:38.726: D/dalvikvm(30509): GC_FOR_ALLOC freed 269K, 42% free 13023K/22312K, paused 45ms, total 45ms
12-24 15:34:38.789: D/dalvikvm(30509): GC_FOR_ALLOC freed 521K, 40% free 13517K/22312K, paused 57ms, total 57ms

ART 结果:
12-24 15:37:51.500: I/art(4735): Background sticky concurrent mark sweep GC freed 27480(1767KB) AllocSpace objects, 35(660KB) LOS objects, 4% free, 39MB/40MB, paused 7.802ms total 68.710ms
12-24 15:37:57.229: I/art(27369): Explicit concurrent mark sweep GC freed 82830(3MB) AllocSpace objects, 10(160KB) LOS objects, 39% free, 19MB/32MB, paused 744us total 84.814ms
12-24 15:37:57.341: I/art(745): Explicit concurrent mark sweep GC freed 47032(2MB) AllocSpace objects, 4(64KB) LOS objects, 24% free, 49MB/65MB, paused 1.161ms total 78.172ms

12-24 15:38:00.259: I/art(5776): Background sticky concurrent mark sweep GC freed 29233(2MB) AllocSpace objects, 34(5MB) LOS objects, 14% free, 28MB/33MB, paused 3.255ms total 265.466ms

从两张图可以清楚的发现,在dalvik模式下刚启动百度地图的几秒内,触发了26次GC事件,总共停顿耗时5371ms。而在ART模式下,可以看到一共触发了4次GC事件,共耗时497.162ms。
对比可以看到,ART下的GC的性能明显提升了,几乎可以说是提升了十倍左右,这是一个数量级的提升,GC环节带来的性能提升还是非常明显。

4 总结

首先通过对dalvik的GC的过程的分析,我们可以发现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表现更好。
并且,通过实验数据的显示我们也可以看到,ART的GC的性能确实有了显著的提升,应用程序的流畅性得到了较好的保证。
以上都只是一个比较初步的分析比较,进一步的原理研究还需要详细学习源码才能融会贯通。

5 参考文档

  1. https://source.android.com/devices/tech/dalvik/gc-debug.html;
  2. http://www.cnblogs.com/jinkeep/p/3818180.html
  3. https://infinum.co/the-capsized-eight/articles/art-vs-dalvik-introducing-the-new-android-runtime-in-kit-kat
  4. http://blog.csdn.net/luoshengyang/article/details/42555483