G1垃圾回收器
1.1 概述
G1(Garbage First) 是一款并行回收的,新生代/老年代都回收的全功能垃圾回收器
G1的思想是区域分代化,垃圾优先
区域分代化: 将堆内存分为一个一个的region,每个region可以是物理上不连续的空间,G1对region进行追踪,衡量每个region回收后的价值和回收所需时间(其实就是region回收的效率,回收后能清除较多空间的region优先级更高)
垃圾优先: 由于G1对垃圾回收的效率更加敏感,因此称G1是垃圾优先
G1的出现基于现代计算机资源的升级,cpu和内存不断增大,为了满足在资源充足的情况下的 低暂停时间和吞吐量 两者兼顾的目标
G1的目标就是: 在保证吞吐量的前提下,尽可能的减少暂停时间.
G1在jdk1.9中被设置为默认垃圾回收器,在jdk1.7/1.8中需要使用jvm参数启用
1.2 优点
- 并行和并发: G1同时兼顾并行和并发两种方式
- 分代收集: G1也属于分代收集算法,但是与传统分代不同的是,G1使用region作为分代的区域,region不在要求每个代在堆内存的连续性和数量.即每个region都是一个小的分代区域,但是region又不固定,可能一个Eden的region在完全回收后,下次这个region就变为老年代.
- 堆空间整理: 不存在内存碎片问题; region之间的回收是基于复制算法的,整体看G1的堆内存回收是基于标记-压缩算法,都不存在内存碎片问题,而且当堆空间越大,基于region的G1的优势越明显
- 可控制的暂停时间:
- G1可以设置一个停顿时间,尽可能的达到这个停顿时间.
- G1在回收region的时候是根据region的回收价值来排序,优先回收价值最高的region,就能保证最大的回收效率
- 拿CMS来对比,G1的回收效率可能比不过CMS最好的时候的延时停顿,但是相比于CMS的最差情况(串行回收),无疑好很多.
- 在其他垃圾回收器(ParNew/CMS)中,多线程的操作是基于jvm的内置线程的,而G1可以借助于应用程序线程.
1.3 缺点
- 在小内存(6-8g下)的场景下,效率并不一定超过CMS
- G1在垃圾回收上耗费的内存及保持程序运行的额外内存都要高于CMS
1.4 G1垃圾回收器相关jvm参数
1.4.1 指定使用G1垃圾回收器
-XX:+UseG1GC
启用G1垃圾回收器,在jdk9之后就不再指定了,默认都是G1
1.4.2 指定Region的大小
-XX:G1HeapRegionSize
指定每个Region的大小,一般为2的幂MB,例如: 2MB 4MB 8MB 16MB 32MB
默认值为堆内存的 1/2000
目的是将堆内存分为 2048 个区域.
例如: 当大小设置为1时, 堆内存就是2G 大小为2则堆内存为4G
1.4.3 指定期望的最大停顿时间
-XX:MaxGCPauseMillis
这个参数用于设置期望的最大停顿时间,jvm会尽可能保证达到这个停顿时间,但是不一定能每次回收都达到这个时间,只能尽可能保证高概率的达到这个停顿时间(90%)
参数默认值为 200ms
如果此参数修改的过小,可能会导致的结果: 比如设置为20ms,G1中有很多个region,默认值200ms可以回收10个region,但是20ms就只能回收1个region,如果此时内存占用速度较高,就会导致region回收的速度跟不上清理的速度,久而久之,当内存不够用时就会触发FullGC,反而增大了停顿时间,所以此参数修改要慎重.
1.4.4 指定并发的STW时的工作线程数
-XX:ParallelGCThreads
并发STW的垃圾回收线程数,最多为8,和CMS中的设置一样
1.4.5 指定并发标记的线程数
-XX:ConcGCThreads
设置并发标记的线程数,默认为-XX:ParallelGCThreads的1/4
1.4.6 指定触发垃圾回收的堆占用阈值
-XX:InitiatingHeapOccupAncyPercent
指定触发垃圾回收的java堆占用率阈值,超过此值出发垃圾回收 类似CMS中的参数
默认值为45
调整此参数策略也可以参考CMS中的解释.
1.4 jdk8中使用G1的步骤
目前jdk8应该还是企业主流版本,因此jdk8的垃圾回收器调优有以下几个步骤(G1):
- 使用jvm参数指定启用G1垃圾回收器
- 指定期望的最大停顿时间
- 指定堆内存大小
其他的就由jvm自动调整就可以,再细节的调优需要根据具体情况调整.
1.5 适用场景
- 大内存,多核处理器的硬件环境下的服务端应用(毕竟是保证吞吐量的前提)
- 即需要低延迟,有需要吞吐量的大内存应用
- 针对替换CMS的场景
- 超过一半的内存在活跃状态
- 对象分配频率和年代提升频率非常高
- GC停顿时间过长(0.5-1s)
1.6 region详解
G1使用region跳出了分代垃圾回收的大框架,开启了分区回收的新概念,其中region是最重要的一部分.
传统分代垃圾回收要求每代都是连续的内存空间
而G1将每个代对应的内存空间都拆分到一个一个region上,那么region其实就不再要求内存上的连续性了.
1.6.1 region的分类
G1中将region分为四类
- Eden 新生代的伊甸园区
- Survivor 新生代的幸存者区
- Old 老年代
- Humongous 存放大对象的区域
其中新生代/老年代的都不难理解,就是将粒度拆分的更细而已.
而新增了一个H区,这个类型主要用来存放大对象,大对象的定义为超过1.5个region
为什么新增这个大对象呢?
在原来的垃圾回收器中,如果一个新对象新生代放不下,那么就会直接进入老年代存放,但是如果这个大对象是一个生命周期很短的对象时,就存在问题,老年代的回收耗时要比新生代更加多.因此在G1中新增H区来处理大对象
注: 当一个H区存放不下一个大对象时,就必须寻找连续的H区来存放.
1.6.2 region的分配
首先 对G1来说,region的分配就是空闲列表的方式,因为region之间内存不连续,我们可以把region理解为一个个碎片,这个时候只能采用空闲列表的方式来分配.
对于region内部的分配来说就分为两种情况:
1.6.2.1 指针碰撞
region内部就是指针碰撞的方式来分配,region内部的垃圾回收算法是基于复制算法的,因此不存在碎片问题,直接使用指针碰撞就可以
1.6.2.2 TLAB
我们之前在堆内存的学习中看到过,堆的内存配置里有一小块是TLAB
主要是为了多线程的并发情况
region内部也存在TLAB,可以通过这样的方式分配内存.
1.7 G1垃圾回收的详细过程
1.7.1 主要环节
主要分为三个环节和一个后备环节
- 年轻代回收 YoungGC
- 老年代并发标记 Concurrent Mark
- 混合回收 Mixed STW
- 后备FullGC(单线程,独占式)
简单说明:
- 年轻代的回收其实跟其他gc没有区别,当年轻代内存不足时,开始回收年轻代的region,然后根据年龄计算是放到幸存者区还是进入老年代.这里指的年轻代是所有的年轻代的region合计内存,也就是说即使G1是分区的,但在分代的角度所有的region还是用分代的理念解释的. 此外,年轻代使用的是复制算法,因此是独占式的回收,需要STW.
- 老年代并发标记是标记可达对象的一个过程,这个过程是并发的,因此不影响用户线程执行,当堆内存达到阈值(默认45%)时,开始并发标记过程
- 在老年代并发标记完成后,就开始混合回收. G1的老年代回收与其他垃圾回收器不一样,不用回收整个老年代,他的回收也是根据region来的,我们在上文中也提到了一个最大停顿时间的参数,根据整个参数G1会调整回收region的数量,以达到设置的最大停顿时间(默认200ms),并且新生代也可以在整个环节进行回收(混合回收)
- 后备方案,就是当G1的老年代回收失败时,就会启用单线程的FullGC来做整个jvm的回收工作,以确保jvm的正常运行,出现FullGC的时候可能就等待时间较长.
1.7.2 记忆集 Remembered Set
记忆集记录了每个region被其他region引用的情况.
记忆集的出现主要是为了解决 跨代引用 问题
跨代引用是指: 新生代的对象被老年代的对象引用
如果出现了跨代引用,那么在垃圾回收的时候,怎么寻找GCRoots? 比如当新生代的对象被老年代的对象引用的时候,回收新生代的时候,怎么寻找该对象是否存在引用,将老年代全部遍历一遍也可以实现,但是开销太大.
记忆集就是为了解决这个问题的.
给每个region维护一个记忆集,记录每个region被其他region引用的情况,然后在垃圾回收的时候把记忆集作为GCRoots的一部分,就可以做到跨代引用然后进行垃圾回收.
1.7.3 YoungGC
- 当Eden内存不足时,会触发YoungGC
- YoungGC会回收Eden和Survivor区
当YoungGC开始时,首先STW,然后创建回收集(Collection Set),记录Eden和Survivor所有的内存分段.
YoungGC详细过程:
- 扫描GCRoots 获取根引用和Rset(记忆集)作为GCRoots
- 更新Rset 处理dirty card queue队列中的card,来更新Rset,保证Rset的最新;
- dirty card queue是在引用赋值的时候放入card的一个队列,这个card记录了对象的详细引用信息,用于保证Rset是最新的引用信息
- 使用dirty card queue的目的就是因为Rset存在多线程问题,如果在赋值的时候就直接更新Rset,可能存在锁的问题,开销更大.
- 处理Rset 识别Rset中被老年代对象引用的新生代对象,这些对象被认为是存活对象.
- 复制对象 采用复制算法将region放入到空闲的region中,这个过程中同时会考虑对象的年龄,如果对象的年龄达到阈值,则会进入老年代.
- 处理引用 处理软/弱/虚/终结器等引用信息.
YoungGC完成后,不存在内存碎片问题,且当复制对象完成后,原来的region会被作为空闲region放入空闲列表中等待使用.
1.7.4 老年代并发标记环节
这个过程类似CMS的并发标记过程,有一些区别,可以理解为G1采用了CMS的并发回收过程,且进行区域回收的改进.
具体流程如下:
- 初始标记 标记GCRoot直接引用的可达对象,这个阶段STW,并会触发一次YoungGC
- 根区域扫描 扫描Survivor区引用的老年代对象,并标记老年代中被引用的对象(视为可达对象),这个过程需要在YoungGC之前完成
- 并发标记 类似CMS,根据初始标记的结果进行并发标记,此时垃圾回收线程和用户线程并行执行,但是可能会被YoungGC打断,在并发标记阶段会同时计算该区域对象的存活率(可达对象占比),另外如果该区域所有对象都是垃圾对象,那么会直接进行清理,不用等待混合回收环节
- 再次标记 类似CMS,修正并发标记环节再次复活的对象.
- 独占清理计算 计算各个区域的存活对象占比,垃圾回收占比并排序,为下个环节混合回收做数据支持,因为计算需要准确性所以是STW的,不会真的回收垃圾
- 并发清理 清理完全空闲的区域.
1.7.5 混合回收
混合回收就是根据并发标记环节计算得出的回收占比,再根据最大停顿时间来回收一定数量的region,同时也会有YoungGC
混合回收有一些说明
- 默认情况下老年代要分8次回收(jvm参数-XX:G1MixedGCCountTarget控制)
- 混合回收的内容包括
- 1/8的老年代内存段
- Eden区
- Survivor区
- 老年代的1/8的内存段是根据垃圾占比排序出来的,优先回收垃圾占比高的内存分段.
- 老年代内存分段的回收也有一个阈值控制,默认65%(jvm参数-XX:G1MixedGCLiveThresholdPercent控制),当内存分段垃圾占比低于65%时,不会回收,因为回收的开销过大,复制算法存存活对象越多,开销越大.
- 老年代内存也可能进行不到8次,G1允许堆内内存有10%(jvm参数-XX:G1HeapWastePercent)的浪费,这就意味着如果进行7次或者更少的垃圾回收之后,堆内垃圾占比小于10%,那么就不会再次回收了,开销很大,回收的垃圾很少,划不来.
1.7.6 FullGC(并非G1每次垃圾回收必经阶段)
FullGC是单线程,独占式的垃圾回收过程,再开发和调优的过程中要尽量避免FullGC的出现.
FullGC的出现有两种情况:
- G1的回收都是基于复制算法的,将一个region复制到另外空闲的region,如果没有空闲的region的时候,就会进行FullGC
- 当堆内存过小的时候可能出现这个问题
- 当停顿时间设置的过小,而堆内存占用速度又过快的时候可能出现这个问题
- G1是存在并发标记阶段的,在这个阶段用户线程和垃圾回收线程交替执行,如果用户线程在这个时候没有足够的内存使用,就会触发FullGC.
1.8 G1的调优
首先G1在回收阶段(YoungGC和混合回收阶段)都是STW的,G1并没有并发回收的特性,但是在ZGC上可能会存在这个特性.
调优的几个注意点:
- 不要设置年轻代的大小 在使用-Xmn -XX:NewRatio等参数设置年轻代的大小后,年轻代的大小就被固定,不能由G1动态调节,而YoungGC又是独占式的回收,如果年轻代的大小固定,那么就会出现期望最大暂停时间参数被覆盖的问题,因为G1不能根据设置的期望最大暂停时间去调节年轻代的大小了.
- 暂停时间的设置不要太小,会影响到吞吐量