22、JVM实战:主流垃圾回收器G1

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 优点

1、 并行和并发:G1同时兼顾并行和并发两种方式;
2、 分代收集:G1也属于分代收集算法,但是与传统分代不同的是,G1使用region作为分代的区域,region不在要求每个代在堆内存的连续性和数量.即每个region都是一个小的分代区域,但是region又不固定,可能一个Eden的region在完全回收后,下次这个region就变为老年代.;
3、 堆空间整理:不存在内存碎片问题;region之间的回收是基于复制算法的,整体看G1的堆内存回收是基于**标记-压缩算法,**都不存在内存碎片问题,而且当堆空间越大,基于region的G1的优势越明显;
4、 可控制的暂停时间:;

1、 G1可以设置一个停顿时间,尽可能的达到这个停顿时间.;
2、 G1在回收region的时候是根据region的回收价值来排序,优先回收价值最高的region,就能保证最大的回收效率;
3、 拿CMS来对比,G1的回收效率可能比不过CMS最好的时候的延时停顿,但是相比于CMS的最差情况(串行回收),无疑好很多.;
5、 在其他垃圾回收器(ParNew/CMS)中,多线程的操作是基于jvm的内置线程的,而G1可以借助于应用程序线程.;

1.3 缺点

1、 在小内存(6-8g下)的场景下,效率并不一定超过CMS;
2、 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):

1、 使用jvm参数指定启用G1垃圾回收器;
2、 指定期望的最大停顿时间;
3、 指定堆内存大小;

其他的就由jvm自动调整就可以,再细节的调优需要根据具体情况调整.

1.5 适用场景

1、 大内存,多核处理器的硬件环境下的服务端应用(毕竟是保证吞吐量的前提);
2、 即需要低延迟,有需要吞吐量的大内存应用;
3、 针对替换CMS的场景;

1、 超过一半的内存在活跃状态;
2、 对象分配频率和年代提升频率非常高;
3、 GC停顿时间过长(0.5-1s);

1.6 region详解

G1使用region跳出了分代垃圾回收的大框架,开启了分区回收的新概念,其中region是最重要的一部分.
传统分代垃圾回收要求每代都是连续的内存空间
而G1将每个代对应的内存空间都拆分到一个一个region上,那么region其实就不再要求内存上的连续性了.

1.6.1 region的分类

G1中将region分为四类

1、 Eden新生代的伊甸园区;
2、 Survivor新生代的幸存者区;
3、 Old老年代;
4、 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 主要环节

主要分为三个环节和一个后备环节

1、 年轻代回收YoungGC;
2、 老年代并发标记ConcurrentMark;
3、 混合回收MixedSTW;
4、 后备FullGC(单线程,独占式);

简单说明:

1、 年轻代的回收其实跟其他gc没有区别,当年轻代内存不足时,开始回收年轻代的region,然后根据年龄计算是放到幸存者区还是进入老年代.这里指的年轻代是所有的年轻代的region合计内存,也就是说即使G1是分区的,但在分代的角度所有的region还是用分代的理念解释的.此外,年轻代使用的是复制算法,因此是独占式的回收,需要STW.;
2、 老年代并发标记是标记可达对象的一个过程,这个过程是并发的,因此不影响用户线程执行,当堆内存达到阈值(默认45%)时,开始并发标记过程;
3、 在老年代并发标记完成后,就开始混合回收.G1的老年代回收与其他垃圾回收器不一样,不用回收整个老年代,他的回收也是根据region来的,我们在上文中也提到了一个最大停顿时间的参数,根据整个参数G1会调整回收region的数量,以达到设置的最大停顿时间(默认200ms),并且新生代也可以在整个环节进行回收(混合回收);
4、 后备方案,就是当G1的老年代回收失败时,就会启用单线程的FullGC来做整个jvm的回收工作,以确保jvm的正常运行,出现FullGC的时候可能就等待时间较长.;

1.7.2 记忆集 Remembered Set

记忆集记录了每个region被其他region引用的情况.
记忆集的出现主要是为了解决 跨代引用 问题
跨代引用是指: 新生代的对象被老年代的对象引用

如果出现了跨代引用,那么在垃圾回收的时候,怎么寻找GCRoots? 比如当新生代的对象被老年代的对象引用的时候,回收新生代的时候,怎么寻找该对象是否存在引用,将老年代全部遍历一遍也可以实现,但是开销太大.
记忆集就是为了解决这个问题的.
给每个region维护一个记忆集,记录每个region被其他region引用的情况,然后在垃圾回收的时候把记忆集作为GCRoots的一部分,就可以做到跨代引用然后进行垃圾回收.

1.7.3 YoungGC

1、 当Eden内存不足时,会触发YoungGC;
2、 YoungGC会回收Eden和Survivor区;

当YoungGC开始时,首先STW,然后创建回收集(Collection Set),记录Eden和Survivor所有的内存分段.
YoungGC详细过程:

1、 扫描GCRoots获取根引用和Rset(记忆集)作为GCRoots;
2、 更新Rset处理dirtycardqueue队列中的card,来更新Rset,保证Rset的最新;;

1、 dirtycardqueue是在引用赋值的时候放入card的一个队列,这个card记录了对象的详细引用信息,用于保证Rset是最新的引用信息;
2、 使用dirtycardqueue的目的就是因为Rset存在多线程问题,如果在赋值的时候就直接更新Rset,可能存在锁的问题,开销更大.;
3、 处理Rset识别Rset中被老年代对象引用的新生代对象,这些对象被认为是存活对象.;
4、 复制对象采用复制算法将region放入到空闲的region中,这个过程中同时会考虑对象的年龄,如果对象的年龄达到阈值,则会进入老年代.;
5、 处理引用处理软/弱/虚/终结器等引用信息.;

YoungGC完成后,不存在内存碎片问题,且当复制对象完成后,原来的region会被作为空闲region放入空闲列表中等待使用.

1.7.4 老年代并发标记环节

这个过程类似CMS的并发标记过程,有一些区别,可以理解为G1采用了CMS的并发回收过程,且进行区域回收的改进.
具体流程如下:

1、 初始标记标记GCRoot直接引用的可达对象,这个阶段STW,并会触发一次YoungGC;
2、 根区域扫描扫描Survivor区引用的老年代对象,并标记老年代中被引用的对象(视为可达对象),这个过程需要在YoungGC之前完成;
3、 并发标记类似CMS,根据初始标记的结果进行并发标记,此时垃圾回收线程和用户线程并行执行,**但是可能会被YoungGC打断,**在并发标记阶段会同时计算该区域对象的存活率(可达对象占比),另外如果该区域所有对象都是垃圾对象,那么会直接进行清理,不用等待混合回收环节
4、 再次标记类似CMS,修正并发标记环节再次复活的对象.;
5、 独占清理计算计算各个区域的存活对象占比,垃圾回收占比并排序,为下个环节混合回收做数据支持,因为计算需要准确性所以是STW的,不会真的回收垃圾;
6、 并发清理清理完全空闲的区域.;

1.7.5 混合回收

混合回收就是根据并发标记环节计算得出的回收占比,再根据最大停顿时间来回收一定数量的region,同时也会有YoungGC
混合回收有一些说明

1、 默认情况下老年代要分8次回收(jvm参数-XX:G1MixedGCCountTarget控制);
2、 混合回收的内容包括;

1、 1/8的老年代内存段;
2、 Eden区;
3、 Survivor区;
3、 老年代的1/8的内存段是根据垃圾占比排序出来的,优先回收垃圾占比高的内存分段.;
4、 老年代内存分段的回收也有一个阈值控制,默认65%(jvm参数-XX:G1MixedGCLiveThresholdPercent控制),当内存分段垃圾占比低于65%时,不会回收,因为回收的开销过大,复制算法存存活对象越多,开销越大.;
5、 老年代内存也可能进行不到8次,G1允许堆内内存有10%(jvm参数-XX:G1HeapWastePercent)的浪费,这就意味着如果进行7次或者更少的垃圾回收之后,堆内垃圾占比小于10%,那么就不会再次回收了,开销很大,回收的垃圾很少,划不来.;

1.7.6 FullGC(并非G1每次垃圾回收必经阶段)

FullGC是单线程,独占式的垃圾回收过程,再开发和调优的过程中要尽量避免FullGC的出现.
FullGC的出现有两种情况:

1、 G1的回收都是基于复制算法的,将一个region复制到另外空闲的region,如果没有空闲的region的时候,就会进行FullGC;

1、 当堆内存过小的时候可能出现这个问题;
2、 当停顿时间设置的过小,而堆内存占用速度又过快的时候可能出现这个问题;
2、 G1是存在并发标记阶段的,在这个阶段用户线程和垃圾回收线程交替执行,如果用户线程在这个时候没有足够的内存使用,就会触发FullGC.;

1.8 G1的调优

首先G1在回收阶段(YoungGC和混合回收阶段)都是STW的,G1并没有并发回收的特性,但是在ZGC上可能会存在这个特性.

调优的几个注意点:

1、 不要设置年轻代的大小在使用-Xmn-XX:NewRatio等参数设置年轻代的大小后,年轻代的大小就被固定,不能由G1动态调节,而YoungGC又是独占式的回收,如果年轻代的大小固定,那么就会出现期望最大暂停时间参数被覆盖的问题,因为G1不能根据设置的期望最大暂停时间去调节年轻代的大小了.;
2、 暂停时间的设置不要太小,会影响到吞吐量;

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: