JVM垃圾收集器与内存分配策略(四)—— Garbage First

Garbage First 收集器

Garbage First(简称G1)收集器时垃圾收集器技术发展史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。G1是一款主要面向服务端应用的垃圾收集器。

HotSpot开发团队最初赋予它的期望是(在比较长期的)未来可以替换掉JDK5中发布的CMS收集器。
作为CMS收集器的替代者和继承人,设计者们希望做出一款能够建立起“停顿时间模型”(Pause Prediction Model)的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这机会已经是实时Java(RTSJ)中的软实时垃圾收集器的特征了。

为了实现这个目标,首先得要有一个思想上的转变,在G1收集器出现之前的其他所有收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向对内任何部分来组成回收集(Collecion Set,一般简称CSet)进行回收,衡量的标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1仍然是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够堆扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的而对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取良好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1M~32M,且应该为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region当作老年代的一部分进行看待。

虽然G1仍然保留新生到和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1收集器之所以能建立可预测时间停顿模型,是因为它将Region作为单次回收的最小单元,即每次回收的空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆进行全区域的垃圾收集。更具体的处理思路是让G1收集器区跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需要时间的经验值,然后再后台维护一个优先级列表,每次很具用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
忽略用户线程运行过程中的动作(如使用写屏障维护记忆表的操作),G1收集器运作过程大致可划分为以下四个步骤:

  • 初始标记(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收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。1

G1收集器运行示意图:
G1收集器运行示意图
从G1开始,最先进的垃圾收集器的涉及到想都不约而同地变为追求能够应付应用的内存分配速率(Allocation Rate),而不追求一次把整个Java堆全部清理干净。这样,应用再分配,同时收集器在收集,只要收集的速度跟得上对象分配的速度,那一切运作的很完美。这种新的收集器设计思路从工程实现上看是从G1开始兴起的,所以说G1是收集器技术发展的一个里程碑。


  1. 原文是:It meets garbage collection pause time goals with a high probability,while achieving high throughput。 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章