一、何为G1收集器
The Garbage-First (G1) garbage collector is a server-style garbage collector, targeted for multiprocessor machines with large memories. It attempts to meet garbage collection (GC) pause time goals with high probability while achieving high throughput. Whole-heap operations, such as global marking, are performed concurrently with the application threads. This prevents interruptions proportional to heap or live-data size.
Garbage First(简称G1)收集器是垃圾收集器技术发展史上里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
作为CMS收集器的替代者和继承人,设计者们希望做出一款能够建立起“停顿时间模型”的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N秒这样的目标。
二、G1收集器内存管理
1、Mixed GC模式
G1的里程碑意义来源于其面向局部收集的设计思路和基于Region的内存布局形式,这也是G1实现其停顿时间模型的底气。在G1收集器出现之前的所有其他收集器,包括被它所替代的CMS,垃圾收集的目标范围都是整个新生代(Minor GC)或整个老年代(Major GC),亦或者是整个Java堆(Full GC)。而G1实现了可以面向堆内存的任何部分来组成回收集。衡量标准不再是分代,而是回收的实际收益,这就是Mixed GC模式。
2、基于Region的堆内存布局
G1基于Region的堆内存布局是它实现Mixed GC的关键。我们不能说G1摈弃了分代理论,相反,G1依然是依据分代理论设计的,但其堆内存布局与其他收集器有非常明显的差异,它不再坚持固定大小以及固定数量的分代区域划分,而是把堆分成多个大小相等的独立区域,称为Region,而每个Region都可能是新生代或老年代。这样无论是针对哪种对象,都可以有比较好的收集效果。
从上图中我们可以看见,Region中还有一种Humongous区域,它专门用来存储大对象。G1认为一个对象的大小超过了一个Region的一半,那就可以称为大对象。如果对象大小超过一个Region,就存储在连续的多个Region当中。另外值得注意的是,G1的大部分行为都把Humongous Region作为老年代的一部分来看待。
3、G1具有优先级的区域回收方式
G1之所以能够建立起可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元。G1收集器会跟踪每个Region中垃圾总的“价值”大小,即回收所获得的空间大小和回收所需时间的经验值,然后在后台维护一个优先级列表。并可以根据用户的设定回收价值收益最大的Region。这也是“Garbage First”其名的由来。
三、G1收集器开发者花大量时间解决的三个尖锐问题
G1收集器作为一款跨时代的收集器,它从发表论文到商用经历了超过十年的研发,其中解决了无数的问题,以下是三个典型且重要的问题向读者说明。
1、跨Region引用问题如何解决?
和其他收集器解决跨代问题的方法一样,G1使用记忆集从而避免全堆作为GC Roots扫描。但不同的是G1的每个Region都维护属于自己的记忆集,它们会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围内。G1的记忆集在存储结构的本质上是哈希表(key是别的Region的起始地址,Value是卡表索引号的集合)。
由于Region的数量较多,而每个Region都有自己的记忆集,所以G1收集器要花费更大的内存来维持工作,这个数通常是Java堆的10%~20%。
2、在并发阶段如何保证收集线程与用户线程互不干扰地运行?
首先,用户线程改变对象引用关系时,必须保证不打破原本的对象图结构,导致标记结果出现错误。CMS对这个问题采取了增量更新的算法进行解决,而G1选择了原始快照(SATB)的方法进行解决。
其次,回收过程中会有新对象需要进行内存分配。G1为每个Region设置了两个名为TAMS的指针,把Region的一部分空间用于并发回收过程中的新对象分配。新对象的地址必须在这两个指针之上。这部分空间被收集器视为默认存货, 不纳入回收范围。
3、怎样建立起可靠的停顿预测模型?
G1收集器的停顿预测模型是以衰减均值作为理论基础来实现的。在垃圾收集过程中,G1收集器会记录每个Region的回收时间、记忆集中的脏卡数量等各个步骤的成本,并按照一定的统计信息和统计算法得出“衰减平均值”。衰减平均值更准确地代表了最近的平均状态,Region的统计状态越新就越能决定回收的价值。
根据这些信息,收集器可以决定应当找出哪些Region进入回收集,最终在不超过期望时间的前提下获得最高收益。
四、G1收集器实际运作的四大步骤
1、初始标记
标记一下GC Roots能直接关联到的对象。并且修改TAMS的值,让并发标记阶段分配对象有据可依。这个阶段需要停顿线程,但耗时很短,并且在Minor GC时同步完成。
2、并发标记
从GC Root中开始对堆中对象进行可达性分析,扫描对象图,找出要回收的对象,这阶段耗时长但是可以和用户程序并发执行。当对象图扫描完成后,还要重新处理SATB记录下的在并发时有引用变动的对象。
3、最终标记
对用户线程做一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那些少量的SATB记录。
4、筛选回收
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,制定具体的回收计划。可以自由地选择多个Region作为回收集,把其中存活的对象复制到空的Region中,再清理掉整个回收集。由于涉及存活对象的移动,所以必须暂停用户线程。
总之,G1收集器的设计目标是在延迟可控的前提下获得尽可能高的吞吐量。
五、G1收集器与CMS收集器的比较
作为两款关注停顿时间的收集器,G1常被作为CMS收集器的比较对象。在今天,G1已经几乎完全取代了CMS的地位,但这并不意味着CMS在G1面前不值一提。
先说明一个事实:在小内存上CMS的表现可能会优于G1,而大内存上G1毫无疑问会占据优势。这个堆内存大小的平衡点通常在6~8GB左右。
1、G1的新设计带来的优势
指定最大停顿时间、分Region的内存布局、按收益确定最终回收集,这些都是G1的新设计给它带来的相对于CMS的优势。
2、整体收集算法的不同
CMS集于标记-清除算法进行收集,而G1从整体看集于标记-整理算法进行收集,局部看基于标记-复制算法进行收集。显然,G1的两种解读方法都意味着它不会产生任何内存碎片。这样的特性有利于程序长久地平稳运行。
3、内存消耗不同
我们前文中提到,G1收集器为每一个Region都提供了卡表作为记忆集,显然这意味着G1相比CMS需要消耗更大量的内存来完成其本职工作。相比之下CMS的卡表仅有一份且实现简单。
4、执行负载不同
CMS使用写后屏障来更新和维护卡表。G1除了使用写后屏障,为了实现快照搜索算法,它还得使用写前屏障来跟踪并发时的指针变化情况。这也引出了原始快照和增量更新两种方法的比较:原始快照能够减少并发标记和重新标记阶段的损耗,避免在标记阶段停顿时间过长,但它同时也会产生由于跟踪引用变化带来的额外负担。
由于G1写屏障的复杂操作要比CMS消耗更多的运算资源,CMS的写屏障实现是直接的同步操作,而G1必须把它实现为类似消息队列的架构,即把写前屏障和写后屏障要做的事都放到队列里,然后异步处理。