G1 (Garbage First) 垃圾回收器是在Java7 update 4之后引入的一个新的垃圾回收器,JDK9 中成为默认垃圾回收器。
G1是 HotSpot为解决CMS算法产生空间碎片和其它一系列的缺陷,而提供的另外一种垃圾回收策略。它是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。
G1的目标是使用当前的目标应用程序和环境在延迟和吞吐量之间达到最佳平衡。
结论
- Java1.7正式启用,Java9 中G1成为默认的垃圾回收器了,如果需要显示启用,使用参数
-XX:+UseG1GC。 - G1 是一个并行回收器,他把堆内存分割成若干Region(物理上不连续),每个Region的大小1-32M不等,但必须是2的整数次幂(通过
-XX:G1HeapRegionSize=size设置)。每个Region可以独立表示Eden、Survivor From、Survivor To、老年代等。 - 每次根据允许的收集时间,优先回收垃圾最多的Region(在后台维护一个优先列表),整体使用标记+整理算法,Region之间使用了复制算法。
- 官方给G1设定的目标是在延迟可控的情况下,获得尽可能高的吞吐量。
G1与CMS区别
- G1从整体上来看是 标记-整理 算法,但从局部(两个Region之间)是复制算法。而CMS是 标记-清除算法 所以说,G1不会产生内存碎片,而CMS会产生内存碎片
- CMS对Java堆内存使用的是传统的 _新生代和老年代划分方法_,而G1使用的全新的划分方法。
- CMS收集器只收集老年代,可以配合新生代的Serial和ParNew收集器一起使用。G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用,G1内存回收是以region作为基本单位的。
- G1 是软实时的(soft real-time)。实时垃圾回收是指在要求时间内完成垃圾回收,软实时则指的是用户可以指定垃圾回收的时间限制,G1会努力在这个时限内完成垃圾回收,但并不保证。
基本概念
Heap Region
本质上说,G1依然是个分代垃圾回收器。不同的是,它引入了额外的概念:Region。
G1垃圾回收器把堆划分成一个个大小相同的Region。在HotSpot的实现中,整个堆默认被划分成2048左右个Region。每个Region的大小在1-32MB之间。
Region的大小必须为2的整数倍,如2MB、4MB、6MB等,可以通过-XX:G1HeapRegionSize参数手动指定,如果G1HeapRegionSize未设置,默认情况下则在堆初始化时计算Region的实际大小。
G1垃圾回收器的分代是建立在这些Region的基础上的。对于Region来说,它会有一个分代的类型,并且是唯一的。即每一个Region要么是young的要么是old的。还有一类十分特殊的Humongous。
所谓的Humongous,就是一个对象的大小超过了某一个阈值——(HotSpot中是Region的1/2),那么它会被标记为Humongous。
如果我们审视HotSpot的其余的垃圾回收器,可以发现这种对象以前被称为大对象,会被直接分配老年代。而在G1回收器中,则是做了特殊的处理。
G1并不要求相同类型的region要相邻。换言之,就是G1回收器不要求它们连续。当然在逻辑上,分代依旧是连续的。如图
其中E代表的是Eden,S代表的是Survivor,H代表的是Humongous,剩余的深蓝色代表的是Old(或者Tenured),灰色的代表的是空闲的Region。
Region可以说是G1回收器一次回收的最小单元。即每一次回收都是回收N个Region。这个N是多少,主要受到G1回收的效率和用户设置的软实时目标有关。
每一次的回收,G1会选择可能回收最多垃圾的Region进行回收。
Remember Set & Card Table & Write Barrier
RS(Remember Set)是一种抽象概念,用于记录从非收集部分指向收集部分的指针的集合。
在传统的分代垃圾回收算法里面,RS被用来记录分代之间的指针。在G1回收器里面,RS被用来记录从其他Region指向本Region的指针情况。
因此,每个Region都有一个RS。这种记录可以带来一个极大的好处:在回收一个Region的时候不需要执行全堆扫描,只需要检查它的RS就可以找到外部引用
(不管是G1还是其他分代收集器,JVM都是使用 记忆集(Remembered Set) 来避免全局扫描。),而这些引用就是initial mark的根之一。
如果一个线程修改了Region内部的引用,就必须要去通知RS,更改其中的记录。为了达到这种目的,G1回收器引入了一种新的结构,CT(Card Table)——卡表。
每一个Region,又被分成了固定大小的若干张卡(Card)。每一张卡,都用一个Byte来记录是否修改过。卡表即这些byte的集合。
每次Reference类型数据写操作时,都会产生一个 写屏障(Write Barrier)暂时去终止操作,然后检查将要写入的引用 指向的对象是否和该Reference类型数据在不同的 Region(其他收集器:检查老年代对象是否引用了新生代对象),
如果不同,通过 卡表(Card Table)把相关引用信息记录到引用指向对象的所在Region对应的记忆集(Remembered Set) 中。当进行垃圾收集时,在GC Roots枚举范围加上记忆集;就可以保证不进行全局扫描了。

G1的记忆集可以理解为一个哈希表,Key就是别的Region的起始地址,Value就是卡表的索引号集合。
因为G1将Java堆划分为一个个Region的缘故,而Region数量相比于传统分代数量明显多得多,所以G1相比于传统的垃圾回收器来说,需要消耗相当于Java堆容量 10%~20%的额外空间来维持收集器的工作。
工作流程
Initial Marking (STW)
初始标记(Initial Marking):这阶段仅仅只是标记GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,
这阶段需要停顿线程,但是耗时很短。而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
Concurrent Marking
并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。
当对象图扫描完成以后,还要重新处理SATB(Snapshot At The Beginning)记录下的在并发时有引用变动的对象。
Final Marking (STW)
最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
Live Data Counting and Evacuation (STW)
筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。
可以自由选择多个Region来构成会收集,然后把回收的那一部分Region中的存活对象复制到空的Region中,在对那些Region进行清空。在该阶段还会重置Remember Set。
除了并发标记外,其余过程都要 STW
YoungGC完整流程
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数-XX:MaxGCPauseMills设定的值,那么增加年轻代的region,继续给新对象存放,
不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数-XX:MaxGCPauseMills设定的值,那么就会触发Young GC。
MixedGC完整流程
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,
正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。
FullGC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。
参数说明
1 | -XX:+UseG1GC:使用G1收集器 |
总结
为什么G1能够让用户设置应用的暂停时间?
根据Garbage First的原则,G1优先处理回收垃圾最大量的区间(Region),G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。
由于内存被分成了很多小块,又带来了另外好处,由于内存块比较小,进行内存压缩整理的代价都比较小,相比其它GC算法,可以有效的规避内存碎片的问题。
缺点:如果应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个Heap的回收,那么G1要做的工作量就一点也不会比其它垃圾回收器少,而且因为本身算法复杂了一点,可能比其它回收器还要差。
G1停顿时间的瓶颈主要是标记-复制中的转移阶段STW,主要是因为G1未能解决转移过程中准确定位对象地址的问题。因此G1比较适合内存稍大一点的应用(一般来说至少4G以上),小内存的应用还是用传统的垃圾回收器比如CMS比较合适。