深入理解JAVA虚拟机读书笔记:G1收集器入门

前言

在官网中,是这样描述G1的:

The Garbage-First (G1) collector is a server-style garbage collector,
targeted for multi-processor machines with large memories. It meets
garbage collection (GC) pause time goals with a high probability,
while achieving high throughput. The G1 garbage collector is fully
supported in Oracle JDK 7 update 4 and later releases. The G1
collector is designed for applications that: Can operate concurrently
with applications threads like the CMS collector. Compact free space
without lengthy GC induced pause times. Need more predictable GC
pause durations. Do not want to sacrifice a lot of throughput
performance. Do not require a much larger Java heap.

从官网的描述中,我们知道G1是一种服务器端的垃圾收集器,应用在多处理器和大内量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。它是专门针对以下应用场景设计的:

  • 像CMS收集器一样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 需要GC停顿时间更好预测。
  • 不希望牺牲大量的吞吐性能。

G1收集器的设计目标是取到CMS收集器,它同CMS相比,在一下方面表现的更出色:

  • G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。
  • G1的Stop The World(STW更可控),G1在停顿时间上添加了预测机制,用户可以执行期望停顿时间。

有了以上的特性,难怪有人说它是一款驾驭一切的垃圾收集器。

G1中的几个重要概念

在G1的实现过程中,引入了一些新的概念,对于实现高吞吐、没有内存碎片、收集时间可控等功能起到了关键作用。

Region

传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间MateSpace),这种划分的特点是各代的存储地址(逻辑地址)是连续的。如下图所示:
深入理解JAVA虚拟机读书笔记:G1收集器入门_第1张图片
而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。如下图所示:
深入理解JAVA虚拟机读书笔记:G1收集器入门_第2张图片
在上图中,我们注意到了还有一些Region标明了H,它代表Humougous,这表示这些Region存储的是巨型对象(H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征:

  • H-obj直接分配到了old gen,防止反复拷贝移动。
  • H-obj在global concurrent marking阶段的cleanup 和full GC阶段回收。
  • 在分配H-obj之前先检查是否超过initiating heap occupancy percent和the marking threshold,如果超过的话,就启动global concurrent making,为的是提早回收,防止evacuation failures 和 full GC。

为了减少连续H-objs分配对GC的影响,需要把大对象便为普通的对象,建议增大Region Size。

一个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围从1M到32M,且是2的指数。
如果不设定,那么G1会根据Heap大小自动决定。

SATB

全称是Snopashot-At-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracting得到的,作用是维持并发GC的正确性。
那么它是怎么维持并发GC的正确性呢?根据三色标记算法,我们知道对象存在三种状态:

  • 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
  • 灰:对象被标记到了,但是它的field还没有被标记或没有被标记完。
  • 黑:对象被标记了,且它的所有field也被标记完了。

由于并发阶段的存在,Mutator和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况,这种情况发生的前提是:

  • 把一个白对象的引用存到黑对象的字段里,如果这个情况发生,因为标记为黑色的对象认为是扫描完成的,不会再对他进行扫描。只能通过灰色的对象
  • 某个白对象失去了所有能从灰对象到达它的引用路径(直接或间接)
    深入理解JAVA虚拟机读书笔记:G1收集器入门_第3张图片

对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别是prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会漏标,从而导致被回收掉,这是非常严重的错误,所以SATB破坏了第二个条件。也就是说,一个对象的引用被替换时,可以通过write barrier将旧引用记录下来。
SATB也是有副总用的,如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage。因为SATB的做法精度比较低,所以造成的float garbage也会比较多。

RSet

全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工作,和Card Table有些类似。还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。
逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于一种points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card范围内。这个RSet其实是一个HashTable,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。
下图表示了RSet、Card和Region的关系:
深入理解JAVA虚拟机读书笔记:G1收集器入门_第4张图片
上图中有三个Region,每个Region被分成了多个Card,在不同的Region中的Card会互相引用,Region1中的Card对象引用了Region2中的Card对象,蓝色实线表示的就是point-out的关系,而在Region2中的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是point-into。
而维系RSet的引用关系靠post-write barrier和Concurrent refinement threads来维护。
post-write barrier记录了跨Region的引用更新,更新日志缓冲区则记录了那些包含更新引用的Cards。一旦缓冲区满了,Post-write barrier就停止服务了,会由Concurrent refinement threads处理这些缓冲区日志。

Pause Prediction Model

Pause Prediction Model即停顿预测模型。
G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?这需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。

GC过程

G1 GC模式

G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。

  • Young GC:选定所有年轻代理的Region。通过空间年轻代的个数,即年轻代内存大小,来控制young GC的时间开销。
  • Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。

由上面的描述克制,Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用Serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供fullGC的。

上文中,多次提到了global concuurent marking,它的执行过程类似CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为四个步骤:

  • 初始标记(Initial mark,STW)。它标记了从GC Root开始直接可达的对象。
  • 并发标记(Concurrent Marking)。这个阶段从GC Root开始对Heap中的对象标记,标记线程与应用线程并发执行,并且手机各个Region的存活对象信息。
  • 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
  • 清除垃圾(Clean up)。清除空Region(没有存活对象的),加入到free list。
    第一阶段initial mark公用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。第四阶段Clean up只是回收了没有存活对象的Region,所以它并不需要STW。

Young GC发生的时机大家都知道,那什么时候发生Mixed GC呢?其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。

  • G1HeapWastePercent:在global concurrent marking结束之后,我们可以知道old gen regions中有多少空间要被回收,在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
  • G1MixedGCLiveThresholdPercent:old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。
  • G1MixedGCCountTarget:一次global concurrent marking之后,最多执行Mixed GC的次数。
  • G1OldCSetRegionThresholdPercent:一次Mixed GC中能被选入CSet的最多old generation region数量。
    原文:https://tech.meituan.com/g1.html

你可能感兴趣的:(JVM)