G1 回收器

G1 内存划分

G1 提供延迟和吞吐量之间的平衡, 有可预测的 gc 停顿时间

  • -XX:+UseG1GC 开启 G1 收集器 (G1 是标记复制, CMS 是标记清除)
  • g1 取消 青年代, 老年代的空间划分, 不必担心每个代内存是否足够. 取而代之的是,将堆划分为若干个区域(Region), 这些区域的身份可以互相转化,分为:
    (1) Eden 区域 (2) Survior 区域 (3) old 区域 (4) 新加的 humongous 区域.
    所以 G1 其实仍然属于分代收集器, 只是这些代不是连续的内存空间, 而且每个 region 的角色会变化.
  • 新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到 old 区域 或者 Survivor区域。G1收集器通过将对象从一个区域复制到另外一个区域的方式,完成清理工作. 而且可以在复制过程中完成堆压缩(至少是部分堆的压缩),这样也就不会有cms的内存碎片问题的存在( CMS 属于标记清楚收集器, 所以会有内存碎片)。
  • CMS 中, 因为有大对象直接产生在老年代的设定, 导致如果有短期存在的大对象, 会导致老年代 gc , 产生 full gc. 为了解决这个问题, g1 增加了 humongous 区域, 如果对象的 size 超过可区域的一半, 则该对象直接产生在 humongous 区域, 如果对象的 size 超过一个 humongous 区域大小, g1 会去寻找连续的几个 humongous 区域 , 这个操作可能产生 full gc

G1 的 gc 策略

G1提供了两种GC模式,Young GC 和 Mixed GC,两种都是 Stop The World(STW) 的。下面我们将分别介绍一下这2种模式。

1. Young GC

默认超过 XX:InitiatingHeapOccupancyPercent=45 进行 young gc (含义在下面的条有参数)

Young GC 主要是对 Eden 区进行 GC,它在 Eden 空间耗尽时会被触发。在这种情况下,Eden 空间的数据移动到 Survivor 空间中,如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到年老代空间。Survivor 区的数据移动到新的 Survivor 区中,也有部分数据晋升到老年代空间中。最终 Eden 空间的数据为空,GC停止工作,应用线程继续执行。

(1) RSet (remember set) 加速年轻代引用扫描
RSet 是一种记录对象之间应用关系的表, 可以加速对无用对象的扫描

  • CMS: 老年代中记录老年代对象引用了哪些青年代对象:
    在CMS中,也有RSet的概念,是在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,仅仅需要扫描这一块区域,而不需要扫描整个老年代查找对象是否被引用。这是 point-out 的做法.
  • G1: 在每个青年带 region 中, 记录引用了哪些老年代的对象:
    在 G1 中,并没有使用 point-out . 因为每个 region 太小,region 数量太多,如果是用 point-out 的话,会造成大量的扫描浪费,有些根本不需要 GC 的 region 引用也扫描了。于是 G1 中使用 point-in 来解决。point-in 的意思是记录哪些 region 引用了当前 region 中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次 GC 时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。

(2) Card Table
需要注意的是,如果引用的对象很多,记录引用地址的开销就会很大. 如果能记录1个连续的地址空间, 就可以用连续地址来记录多个引用对象, 减少引用地址的开销,在 G1 中又引入了另外一个概念,卡表(Card Table)。一个 Card Table 将一个 region 在逻辑上划分为固定大小的连续区域,每个区域称之为卡(card), 大小介于 128 到 512 字节之间。CardTable 通常为字节数组,由 Card 的索引(即数组下标)来标识每个 region 的空间地址。默认情况下,每个卡都未引用。当一个地址空间有引用时,这个地址空间对应的数组索引的值被标记为"0",即标记为脏引用,此外 RSet 也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的 Region 的起始地址,Value 是一个集合,里面的元素是 Card Table 的 Index。

2. mixed gc

mixed gc = 正常的新生区域 gc + 扫描线程标记好的 old 区域
gc步骤分为 2 步: 全局并发标记 (global concurrent marking) + 拷贝压缩存活对象 (evacuation)
全局并发标记分为 4 步:
上文中,多次提到了global concurrent marking,它的执行过程类似CMS,但是不同的是,在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为四个步骤:

  • 初始标记(initial mark,STW): 它标记了从GC Root开始直接可达的对象。
  • 并发标记(Concurrent Marking): 这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
  • 最终标记(Remark,STW)。标记那些在并发标记阶段发生变化的对象,将被回收。
  • 清除垃圾(Cleanup)。清除空Region(没有存活对象的),加入到free list。
    该阶段Cleanup只是回收了没有存活对象的Region,所以它并不需要STW。

第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随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 数量。

并发标记阶段的三色标记法:
三色标记可以推演回收器的正确性。首先,我们将对象分成三种类型的。
(1) 黑色: root 对象,或者该对象与它的子对象都被扫描
(2) 灰色: 对象本身被扫描,但还没扫描完该对象中的子对象
(3) 白色: 未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象

当GC开始扫描对象时,按照如下步骤进行对象的扫描:
(1) 根对象被置为黑色,子对象被置为灰色。
(2) 继续由灰色遍历,将已扫描了子对象的对象置为黑色。
(3) 遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。 这个过程看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题. 比如对象 c 已经被标记为无用对象, 但在并发标记过程中, 应用程序有吧对象 C 关联到对象 A 的属性 A.c = c. 如何保证GC标记的对象不丢失呢?有如下2种可行的方式:

  • 插入的时候记录对象
  • 删除的时候记录对象

刚好这对应CMS和G1的2种不同实现方式:

  • 在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。
  • 在G1中,使用的是SATB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:
    1. 在开始标记的时候生成一个堆的逻辑"快照" (对象引用图)
    2. 在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)
    3. 可能存在游离的垃圾,将在下次被收集

这样,G1到现在可以知道哪些老的 region 可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:

3. full gc for g1

full gc下的退化
某些情况下, g1 触发 full gc, 退化为使用单线程的 Serial 收集器完成垃圾清理:
(1) 并发模式失败
在进行 mixed gc 之前, old 区域就被沾满, 则 g1 会跳过并发标记周期. 这种情况下需要增加堆大小或调整周期. 例如增加线程数 -XX:ConcGCThreads
(2) 晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在 gc 日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:
* a. 增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
* b. 通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
* c. 也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。
(3) 巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大 -XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

g1 如何达到限制 gc 停顿时间的目标

参数 XX:MaxGCPauseMillis=200 设置用户期望的 gc 停顿时间. G1怎么满足用户的期望呢?就需要这个停顿预测模型. 该模型通过历史数据, 预测本次 gc 要选择的 region 数量, 通过控制进行 gc 的 region 数量, 来满足用户期望. 在G1 GC 过程中,每个步骤花费的时间都记录其衰减均值、衰减变量,衰减标准偏差等, 最后根据一个公式预测本次 gc 要选择的 region 数量. 需要进行 gc 的 region 加入 CSet 中. (CSet是为了满足控制停顿时间而设计的筛选机和)

调优参数

-XX:G1HeapRegionSize=n: 设置的 G1 一个 region 的大小。值是 2 的幂,范围是 1 MB 到 32 MB 之间。目标和默认数值是根据 Java 堆的大小划分出 2048 个 region

-XX:ParallelGCThreads=n: 设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。n 的值与逻辑处理器的数量相同,最多为 8。如果逻辑处理器不止八个,则将 n 的值设置为逻辑处理器数的 5/8 左右。这适用于大多数情况,除非是较大的 SPARC 系统,其中 n 的值可以是逻辑处理器数的 5/16 左右。

-XX:ConcGCThreads=n: 设置并行标记的线程数。将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

-XX:InitiatingHeapOccupancyPercent=45
设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

避免使用以下参数:
避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。

上一篇:HBase二级索引、读写流程


下一篇:STP进阶版MSTP