G1收集器详解

1.基础介绍

1.1 G1简介

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

G1是一款主要面向服务端应用的垃圾收集器。HotSpot开发团队最初赋予它的期望是(在比较长期的)未来可以替换掉JDK5中发布的CMS收集器。JDK9发布之日,G1宣告取代了Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落被声明为不推荐(Deprecate)使用的收集器。

G1是一个实现了可控停顿时间的垃圾收集器,通过-XX:MaxGCPauseMillis参数进行设置,默认是200ms。

1.2 Region

G1开创的基于Region的堆内存布局。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

G1收集器详解_第1张图片
image

G1中五种不同类型的Region:

  1. Eden regions(年轻代-Eden区)
  2. Survivor regions(年轻代-Survivor区)
  3. Old regions(老年代)
  4. Humongous regions(巨型对象区域,通常也被认为是老年代的一部分)
  5. Free resgions(未分配区域,也会叫做可用分区)-上图中空白的区域

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

因此,大对象分配空间分配区域策略可总结为:

对象大小为:<0.5个Region,存储到标记为Eden的Region中
对象大小为:0.5~1个Region,存储到标记为Humongous的Region中
对象大小为:>1个Region,存储到标记为Humongous的多个连续Region中

1.3 GC类型

  • youngGC:回收Eden区和Survivor区的内存。
  • MixedGC:包含所有年轻代以及部分老年代Region。
  • FullGC:全堆扫描。

2.运行原理

2.1 基础知识

2.1.1 记忆集(Remenbered Set,简称RSet)和卡表(Card Table)

记忆集(RSet)是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。卡表(Card Table)就是记忆集的一种具体实现,它定义了记忆集的记忆精度、与堆内存的映射关系等。关于卡表与记忆集的关系,可以用HashMap与Map的关系来类比理解。

卡表最简单的形式可以只是一个字节数组,数组中的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,整个内存块被称为“卡页”(Card Page)。HotSpot中卡页大小为521字节。如果卡表标识内存区域的起始地址是)0x0000的话,数组的第0、1、2号元素,分别对应了地址范围0x0000 ~ 0x01FF、0x0200 ~ 0x03FF、0x0400 ~ 0x05FF的卡页内存块,如下图所示。

G1收集器详解_第2张图片
image

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就对应卡表的数组元素的值标识为1,成为这个元素变脏,没有则标识为0.在垃圾收集发生时,只要筛选出卡表中变脏的元素,旧能轻易得出哪些卡页内存块中包含跨代指针,把它们加入GC Roots中一并扫描。

因此,RSet存在的意义就是避免对象跨代引用时对整个堆内存对象的扫描,起到一种类似索引(更像空间索引)的作用。

2.1.2 原始快照(Snapshot At The Beginning,SATB)

简洁地来说,SATB是维持并发GC的一种手段,因为像CMS、G1等收集器,首先经过初始标记,然后进行并发标记,并发标记过程中,可能对初始标记的结果产生了改动,需要进行修正,分别有增量更新和原始快照两种解决方法。CMS收集器使用增量更新来纠正对象引用关系,而G1收集器使用原始快照的策略。

具体参考:

  • https://www.jianshu.com/p/8d37a07277e0
  • 《深入理解Java虚拟机第三版》p89页

2.2 运行流程

2.1.1 初始标记

仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用地Region中分配新对象。这个阶段需要停顿线程(会产生STW),而且是借用进行Minor GC地时候同步完成的,所以G1收集器在这个阶段没有产生特别明显的停顿。

2.2.2 并发标记

从GC Root开始对堆中对象进行可达性分析,集合RSet,递归扫描堆里的对象图,找出要回收的对象,这个阶段耗时较长,但可与用户程序并发执行。当对象扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

2.2.3 最终标记(重新标记)

对用户线程做另一个短暂的暂停(会产生STW),用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录。

2.2.4 筛选回收

负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后决定回收的那一部分Region的存货对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的曹祖哟涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

3.总结

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

G1提高效率的点有哪些:

  1. 使用RSet降低了扫描的范围。
  2. 重新标记阶段使用SATB速度比CMS的增量更新快。
  3. 清理过程中,选择部分回收价值高的Region进行清理(mixedGC),而不是所有Region,提高了清理效率。

4. 参考

  • https://www.bilibili.com/video/BV13J411g7A1?from=search&seid=3010204080299746856
  • 《深入理解Java虚拟机第三版》

你可能感兴趣的:(G1收集器详解)