1、简介
G1全称Garbage First,是垃圾收集器发展历史上里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
作为CMS收集器的替代者,设计者希望做出一款支持”停顿预测模型”(Pause Predictioin Model)的收集器。在G1收集器出现之前,包括CMS在内,垃圾收集的目标要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存发的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
2、G1堆内存布局

G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
上图中的H区(Humongous)专门用来存储大对象。G1认为只要大小超过一个Region容量的一半就可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于超过了整个Region容量的超级大对象,将会被存放在N个连续的H区中,G1的大多数行为把H区作为老年代的一部分来看待。
“G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认为200ms),优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。”
3、实现难点
3.1、如何解决跨代引用
G1收集器同样使用记忆集来解决跨代引用问题,但在G1收集器上记忆集的应用其实要复杂的多,它的每个Region都需要维护自己的记忆集。因此G1收集器比其他的传统垃圾收集器有着更高的内存占用负担。根据经验,G1至少要耗费Java堆容量的10%至20%的额外内存来维持收集器工作。
3.2、如何并发标记
- G1使用原始快照(SATB)方式解决三色标记法可能出现的错标问题
- G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置之上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。
3.3、如何建立可靠的停顿预测模型
G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的。在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。
4、收集过程

G1收集器的运作过程大致分为以下四个步骤:
4.1、初始标记
- 标记GC Roots能直接关联到的对象
- 修改TAMS的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新内存。
- STOP THE WORLD
4.2、并发标记
- 从GC Root开始对堆中对象进行可达性分析,耗时较长,但可与用户线程并发执行
4.3、最终标记
- 处理并发标记期间发生变动的对象引用(原始快照法)
- STOP THE WORLD
4.4、筛选回收
- 更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间制定回收计划。可自由选择任意多个Region构成回收集,把要回收的那一部分Region的存活对象赋值到空的Region中,再清理掉整个旧Region的全部空间。
- STOP THE WORLD
5、与CMS对比
5.1、优点
- 可指定最大停顿时间
- 分Region的内存布局
- 按受益动态确定回收集
- 不会产生内存碎片
5.2、缺点
- 内存占用
G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其它内存消耗)可能会占用整个堆内存的20%或者更多。
相比而言,CMS的卡表就相当简单,只有唯一一份,而且只用处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕死的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的(代价是当CMS发生Old GC时,要把整个新生代作为GC Roots来进行扫描)。 - CPU负载
G1对写屏障的复杂操作要比CMS消耗更多的运算资源。所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障要做的事情都放到队列里,然后再异步处理。
目前在小内存应用上CMS的表现大概率会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的java堆平衡点通常在6GB至8GB之间。
6、参考
1、《深入理解JAVA虚拟机》
-------------本文结束,感谢您的阅读-------------