一 概述
Garbage First(简称G1)收集器时基于Region的内存分布形式,在JDK8 Update 40的时候,G1提供了并发的类卸载的支持,补全了其计划的功能。这个版本以后的G1收集器被Oracle官方称为"全功能的垃圾收集器"(Fully-Featured Garbage Collector)。
G1是一款主要面向服务端应用的垃圾收集器,在以后的发展中会替换掉JDK5中发布的CMS收集器。在JDK9中,G1取代了Parallel Scavenge+Parallel Old组合,称为服务器模式下的默认垃圾收集器,而CMS垃圾收集器成为不推荐使用(Deprecate)的收集器。
Java HotSpot(TM) 64-Bit Server VM warning: Option UseConcMarkSweepGC was deprecated in version 9.0
该提示为在JDK9以上版本的HotSpot虚拟机使用参数-XX:+ UseConcMarkSweepGC来开启CMS收集器时的警告信息,提示CMS未来将会抛弃。
二 Garbage First收集器
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么为整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1垃圾收集器使用Mixed GC模式可以面向堆内存任何部分来组成回收集(Collection Set,一般简称为Cset)进行回收,衡量标准不再是它属于哪个年代,而是哪块内存中存放的垃圾数最多,回收收益最大。
G1基于Region的堆布局时它能够实现这个目标的关键。虽然G1仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为大小相等的独立区域(Region),且每一个Region都可以根据需要扮演新生代的Eden空间,Survivor空间或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活一段时间,熬过多次收集的旧对象都能获取很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且为2的N次幂。而对于那些超过整个Region容量的超级大对象,将会被存放再N个连续的Humongous Region中,G1的大多数行为都把HumonGous Region作为老年代的一部分来进行看待。
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列无序连续区域的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单词回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
更具体的思路为让G1收集器去跟踪各个Region中的垃圾堆积的"价值"大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后再后台维护一个有限级列表,每次根据用户设定允的收集停顿时间(通过-XX:MaxGCPauseMillis指定,默认值为200毫秒),优先处理回收价值收益最大的Region,这也是"Garbage First"名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式保证了G1收集器再有限的时间内获取尽可能高的收集效率。
三 Garbage First收集器的运作过程
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一个阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时比较短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记(Concurrent Marking):从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时比较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
- 筛选回收(Live Data Counting and Evacuation): 负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
从Oracle官方透露出来的信息可获知回收阶段(Evacuation)其实本也有想过设计成用户程序一起并发执行,但这件事做起来比较复杂,考虑到G1只是回收一部分Region,停顿时间是用户可控制的,所以并不迫切去实现,而选择把这个特性放到G1之后出现的低延迟垃圾收集器(即ZGC)中。另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。
G1中由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。因为G1是需要暂停用户线程来复制对象的,这个停顿时间再怎么低也得有个限度。它默认的停顿目标为200毫秒,很可能出现的结果是由于停顿时间太短,导致每次选出来的回收集只占内存很小的一部分,收集器收集的速度主键跟不上分配器分配的速度,导致垃圾慢慢堆积。很可能一开始收集器还能从空闲的堆内存中获取一些喘息的时间,但应用运行时间一常就不行了,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会比较合理。
四 Garbage First同CMS的比较
G1同CMS相比有不少的优点,如指定最大停顿时间,分Region的内存布局,按收益动态确定回收即这些创新设计带来的红利,单单从最传统的算法理论上看,G1也更有发展潜力。与CMS的"标记-清除"算法不同,G1从整体来看是基于"标记-整理"算法来实现的收集器,但从局部(两个Region之间)上看又是基于"标记-复制"算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能够提供规整的可用内存。这种特性有利于程序长时间运行,再程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。
当然在某些应用场景中G1也存在不足,如在用户程序运行过程中,G1在为垃圾收集产生的内存占用(FootPoint)和程序运行过程时的额外执行负载(Overload)都要比CMS要高。
就内存的角度来说,虽然G1和CMS都是用了卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占用整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相对简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。
从执行负载的角度来说,同样由于两个收集器各自的细节实现特点导致了用户程序运行时的负载会有不同,譬如它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结果复杂,其实是更繁琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用带来的额外负担。由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现的是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。