G1 与 ParNew + CMS 收集器

一. ParNew + CMS

ParNew + CMS 的组合,是用 ParNew 进行年轻代垃圾收集,用 CMS 进行老年代垃圾收集

1. ParNew 青年代

ParNew 是使用多条线程并行执行垃圾回收的算法,会阻塞用户线程的执行,发生 Stop The World。它是除了 Serial 收集器外,唯一能够和 CMS 老年代收集器配合的。后者是 jdk 第一次实现了用户线程与 gc 线程同时工作的收集器。

2. CMS 老年代

CMS(Concurrent Mark Sweep)- 中文并发标记清除收集器,是为了达到最短停顿时间为目标的收集器。从名字上看出,该收集器的2个特点:并发和基于标记清除。CMS 老年代 gc 分为4步

  • 初始标记(initial mark):只标记 GC Root 直接关联的对象
  • 并发标记(concurrent mark):进行 GC Root Tracing,找到所有存活对象和垃圾对象
  • 重新标记(remark):由于上一步 gc 线程和用户线程都在执行,可能在标记过程中,用户线程修改了对象的引用,这一步需要修正找回被用户线程重新引用的垃圾对象,和被用户线程取消引用的存活对象。因此要触发 stop the world,停顿时间比初始标记长,但比并发标记短。
  • 并发清除(concurrent sweep):不阻塞用户线程的清除标记对象

步骤中,带有 concurrent 的是 gc 线程可以和用户线程一起执行的,即 CMS 只有初始标记重新标记阶段会触发 stop the world。尽管 CMS 通过最大程度的并行大大降低了停顿时间,但自身仍有3个不足:

  • (1)CMS 收集器要运行在多核 CPU 上
    CMS 收集器默认启动的 gc 线程个数为 (nCpu + 3)/4,所以在 cpu 核数大于4时,gc 线程所占 cpu 大于等于 25%,cpu 核数越少,gc 线程个数所占 cpu 比重就越大,导致即使在并发阶段,用户的执行效率也会降低最少 25%
  • (2)CMS 无法处理浮动垃圾
    由于 CMS 的垃圾清除阶段, gc 线程和用户线程也是一起执行的,这会导致在垃圾清除的过程中,用户线程有产生了新的垃圾对象,这些垃圾对象由于没有经过标记导致本次 gc 过程无法处理,因此称之为“浮动垃圾”。由于存在浮动垃圾,不能等到老年代沾满再出发 major gc,而是要预留一部分给并发清除阶段使用。1.6中默认老年代占满 92% 触发 major gc,该比例也由参数-XX:CMSInitiatingOccupancyFraction 配置,该参数不能设置太高,否则老年代空间预留不足,导致并发清除阶段产生 “Concurrent Mode Failure”,这会退化为 Serial 收集器回收,停顿时间就长了。
  • (3)“标记清除”收集器会产生大量内存碎片
    碎片过多,会给大对象分配带来麻烦,往往老年代还有很多空间剩余,但无法找到合适的连续空间分配大对象,不得不触发一次 full gc。为了解决这个问题, jvm 提供一个参数 -XX:+UseCMSCompactAtFullCollection ,在因为找不到合适空间而进行 full gc 之前,先对老年代进行一次碎片整理,当然者仍然会增加停顿时间。

二. G1 收集器

G1 提供延迟和吞吐量之间的平衡, 有可预测的 gc 停顿时间。使用-XX:+UseG1GC 开启 G1 收集器。G1 可以理解为既能回收青年代,又能回收老年代 ,尽管在 G1 中已经没有“代”的概念。
g1 将内存化整为零,取消 青年代, 老年代的空间划分, 取而代之的是,将整个堆划分成2048个Region,每个Region的大小在1-32MB之间,具体多大取决于堆的大小。这样可以不必担心原先每个代的内存是否足够。region 分为四种类型:
(1) Eden 区域 (2) Survior 区域 (3) old 区域 (4) 新加的 humongous 区域
所以 G1 其实仍然属于分代收集器, 只是这些代不是连续的内存空间。Region 是 G1 回收器一次回收的最小单元。即每一次回收都是回收 N 个 Region。 N 是多少,主要受用户设置的期望停顿时间有关。每一次的回收,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是为了满足控制停顿时间而设计的筛选机和)

三. 调优参数对比

  1. CMS

    • 可以设置年轻带和老年代大小的比例
    • 可以设置老年代占用多少就触发 magor gc,
    • 可以设置 full gc 前开启压缩
  2. G1

    • 可以设置用户期望的停顿时间

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

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

    • 设置非 STW 阶段的 gc 线程数
      -XX:ConcGCThreads=n,将 n 设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。

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

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

四. CMS 与 G1 对比

CMS 虽然是最小停顿时间的收集器,但因为其

  • 有内存碎片
  • 并发清除对象无法处理浮动垃圾等问题

会导致意外的 full gc 情况出现,表现不稳定。 对于G1 收集器

  • 没有内存碎片
  • 停顿时间可控
  • region 的化整为零取消了因为分代设置不合理导致的 gc
  • mix gc 的全局并发标记 + STW 的拷贝压缩对象没有浮动垃圾
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值