G1垃圾回收器详解与回收性能优化
一、G1的引出
1、G1与堆内存
新生代还是老年代,Stop the World
是最大的痛点,它们都会产生这个现象,影响系统的运行,所有垃圾回收器的优化都是朝着减少STW的目标去做的,G1便应运而生了。G1可以同时回收新生代和老年代,它的最大特点,就是把Java堆内存拆分为多个大小相等的Region
,如图,所以G1的新生代老年代是一种逻辑上的概念了,新生代可能包含了某些Region
,老年代可能包含了某些Reigon
。另一个最大特点,就是允许我们设置一个垃圾回收的预期停顿时间,比如我们可以指定1小时内回收垃圾的时候产生的STW时间要小于1min
G1相比之前的垃圾回收器,最大进步就是STW可控
给整个堆内存设置了大小后,启动JVM,一旦发现你使用的是G1垃圾回收器(通过使用-XX:+UseG1GC
这个参数来设置),那就会自动用堆内存大小除以2048(默认情况下是这个,当然我们可以通过-XX:G1HeapRegionSize
参数来指定),因为最多可以有2048个Region,Region的大小为2的倍数,比如堆内存给2G,那Region可能是1MB、2MB、4MB这样的,堆内存给4G,那每个Region就是2MB。
Region区域既有新生代,也有老年代,这时候就不需要去给他们分配内存了,这两个区域是由G1控制,不停变动的。默认新生代堆内存占比是5%,当然可以通过-XX:G1NewSizePercent
参数来设置新生代初始占比,一般都是维持这个默认值,因为系统运行时会动态变化。
此外,新生代还是有Eden和Survivor划分的,之前有个参数是-XX:SurvivorRatio=8
,意思是说80%的Eden,20%的Survivor,在这里,比如新生代初始共100个Region,那就是80个Eden,两个Survivor各占十个。随着动态分配,比如新生代的Region不断增加,那么Eden和Survivor对应的Region也会不断增加。
2、G1如果做到对于系统的停顿可控的?
G1要做到这一点必须追踪每一个Region的回收价值,所谓回收价值就是根据设定的预期系统停顿时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收,保证GC对系统停顿的影响在可控范围内,同时还能尽可能回收最多的对象。(有点类似贪心算法)。
二、G1的垃圾回收机制
1、G1新生代Region的垃圾回收
前提:新生代占据了整个堆大小的60%。(比如我们划分了2000个Region,差不多有1200个新生代Region,其中Eden占1000个,每个Survivor各占100个,如图)
这时候还是会触发Minor GC,使用复制算法,进入Stop the World
,把Eden中活着的对象放进S1对应的Region,然后回收。看起来和ParNew没区别,其实是有区别的,因为我们给G1设定了停顿时间(参数-XX:MaxGCPauseMills
,默认200ms),那么G1首先会对每个Region追踪回收的时间,再选择,来尽可能多回收掉一点对象。
当然也有进入老年代的几种情况:
-
第一种是超过我们设定的年龄阈值的对象,就会进入老年代Region;
-
第二种是存活的对象超过了Survivor的50%(动态年龄判断规则)
对于大对象的处理,不放入老年代!!G1提供了专门的Region来存放大对象(并不是60%新生代,40%老年代,动态变化的,G1会自己处理),只要一个对象超过了一个Region大小的50%,就会被放过去,这个对象如果太大,还可以横跨多个Region来存放。另外大对象的回收是跟着新生代老年代的回收一起进行的。
2、G1混合垃圾回收——Mixed GC
(1)Mixed GC的触发时机
G1有一个参数,是-XX:InitiatingHeapOccupancyPercent
,他的默认值是45%,这个参数的意思是如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。
(2)Mixed GC的停止时机
混合回收都是基于复制算法进行的,把要回收的Region区存活的对象放入其他Region,然后这个Region全部清理掉,这样就会不断空出来新的Region,有一个参数-XX:G1HeapWastePercent
,默认值是5%,就是说空出来的区域大于整个堆的5%,就会立即停止混合回收了。正常默认回收次数是8次,但是可能到了4次,发现空闲Region大于整个堆的5%,就不会再进行后续回收了。
(3)回收失败问题
可以看出G1整体都是基于复制算法进行,不会出现内存碎片问题,但另一个问题是,Mixed GC中新生代、老年代都是复制算法,对象复制时候别的Region内存不够了咋办?那就是回收失败了!就会立即停止系统程序,然后采用单线程去标记、清理、压缩整理,再空闲出新的Region,这个过程极其缓慢!(采用的是Serial Old回收器)
3、Mixed GC的四个阶段
(1)初始标记阶段
这个过程需要进入Stop the World
的,仅仅只是标记一下GC Roots直接能引用的对象,这个过程速度是很快的。如下图,先停止系统程序的运行,然后对各个线程栈内存中的局部变量代表的GC Roots,以及方法区中的类静态变量代表的GC Roots,进行扫描,标记出来他们直接引用的那些对象。
(2)并发标记阶段
这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,并对这个过程对象的变化做记录,比如哪些对象失去了引用,哪些对象是新建的。如下图所示。(这个阶段也是很耗时的,要追踪全部存活的对象,但跟系统并发运行,影响不大)
(3)最终标记阶段
这个阶段会进入Stop the World
,系统程序是禁止运行的,但是会根据并发标记阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象,如下图所示。
(4)混合回收阶段
基于复制算法,这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内。
注意,这里到底回收哪些Region是G1自己选择的,这里的混合回收是指在我们指定的时间(比如200ms)内回收尽可能多的垃圾。
另外,这个阶段G1允许多次执行混合回收,也就是说先停止系统工作,执行回收,恢复系统运行,再停止系统运行,再回收,再恢复…这么一个流程。每次回收的间隔是由G1自己控制的,回收执行次数可以通过参数-XX:G1MixedGCCountTarget
来设置,这个参数默认回收次数是8次,同时有一个参数-XX:G1HeapWastePercent
,默认值是5%,就是说空出来的区域大于整个堆的5%,就会立即停止混合回收了。正常默认回收次数是8次,但是可能到了4次,发现空闲Region大于整个堆的5%,就不会再进行后续回收了。这种多次回收的机制能够让系统停顿时间不要太长,可以在多次回收的间隙也运行一下。
4、G1垃圾回收的参数
-XX:+UseG1GC
:设置使用G1垃圾回收器-XX:MaxGCPauseMills
:设定系统停顿时间,默认200ms-XX:G1HeapRegionSize
:设置区域划分的个数和大小,默认堆大小/2048
-XX:G1NewSizePercent
:用来设置新生代初始占比的,默认值为5%即可。-XX:G1MaxNewSizePercent
:用来设置新生代最大占比的,默认值为60%即可。-XX:SurvivorRatio=8
:设置新生代Region区域中Eden和Survivor的比例,默认8:1:1-XX:InitiatingHeapOccupancyPercent
:设置Mixed GC的比例,默认45%-XX:G1MixedGCCountTarget
:混合回收阶段最多允许G1执行回收的次数,默认是8次。-XX:G1HeapWastePercent
:默认值5%,Mixed GC时空出来的Region大于5%,就停止混合回收。-XX:G1MixedGCLiveThresholdPercent
:默认值是85%,确定要回收的Region的时候,必须是存活对象低于85%的Region才可以回收。
三、G1性能优化
1、背景引入
百万级用户的在校教育平台,首先分析这个系统最高频的行为。作为用户,浏览课程详情、下单付费、选课排课,这些都是绝对的低频行为,我们几乎不用考虑到系统的运行中去,可以暂时忽略掉。对于这样的一个系统,他最关键的高频行为只有上课!就是每天晚上那两三小时的高峰时期,几乎你可以认为每天几十万日活用户(那些小孩儿)都会集中在这个时间段来平台上上在线课程。所以这个晚上两三小时的时间段里,将会是平台每天绝对的高峰期。那哪个功能最常用呢?除了上课学习,就是互动了。
分析这个系统核心点就是搞明白在晚上两三小时高峰期内,每秒钟会有多少请求,每个请求会连带产生多少对象,占用多少内存,每个请求要处理多长时间。
假设晚上3小时有60w活跃用户,按平均每个用户1小时上课,每小时20w用户,对于每个用户1分钟1次互动,1分钟60次,20万人1分钟就是1200万次互动,平均每秒3000次,也就是1秒承受3000次请求。假设我们使用的是4核8G的机器,差不多需要5台,每台1秒抗住600个请求。互动过程一般不会有复杂对象,算上连带对象也就占几KB,假设5KB,1秒就是3MB左右内存(5*600/1000)。
分配4G给堆内存,其中新生代默认初始占比为5%,最大占比为60%,每个Java线程的栈内存为1MB,元数据区域(永久代)的内存为256M,新生代初始占比和最大新生代占比维持默认值即可,不用设置,分别为5%和60%,此时JVM参数如下:
-Xms4096M -Xmx4096M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M
-XX:+UseG1GC
此时每个Region大小为4G/2048 = 2MB,新生代占5%,算它100个Region,200MB,停顿时间我们先不设置,使用默认值200ms。此时大概不到1分钟就塞满这100个新生代Region了,此时你会觉得由于GC是很灵活的,他会根据你设定的gc停顿时间给你的新生代不停分配更多Region,然后到一定程度,感觉差不多了,就会触发新生代gc,保证新生代gc的时候导致的系统停顿时间在你预设范围内,这是它的一个原理,但事实不是这样的,具体情况要通过工具去查看。
2、优化
对于新生代,主要是避免短生命对象进入老年代
- 预估每次Minor GC后存活下来对象的大小,合理的设置Survivor区,同时考虑高峰期间时,动态年龄判断条件的影响,不要让这种短生命周期对象侥幸逃脱进入老年代
- 大对象有他自己的Region,不用操心
对于老年代
- 系统的停顿时间时关键!是核心,要预测停顿时间,并不是越小越好,过小则回收效果不大
(1)-XX:MaxGCPauseMills 参数优化
这个参数是核心点!如果参数设置的值很大,导致系统运行很久,新生代可能都占用了堆内存的60%了,此时才触发新生代GC,那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象(或是动态年龄判定规则),就会进入老年代中。如果参数设置过小,即使GC停顿时间很短,但GC频率太大,比如说30秒触发一次新生代gc,每次就停顿30毫秒,这样也是很影响系统性能的。至于到底如何优化这个参数,要结合工具的实战演练。
(2)Mixed GC优化
优化Mixed GC并不是优化它的参数,因为它的参数太多了,尽量避免对象过快进入老年代,尽量避免频繁触发Mixed GC,就可以做到根本上优化Mixed GC了。这边核心还是-XX:MaxGCPauseMills
这个参数。如上所说
四、G1的适用场景与总结
1、适合超大内存机器
如果内存是一个大堆,比如部署在有16G、32G的内存的机器上,比如类似Kafka、Elasticsearch之类的大数据相关的系统,都是部署在大内存的机器上的,此时如果你的系统负载非常的高,比如每秒几万的访问请求到Kafka、Elasticsearch上去。那么可能导致你Eden区的几十G内存频繁塞满要触发垃圾回收,假设1分钟会塞满一次。如果使用传统回收器(比如ParNew+CMS),不用G1,会导致新生代每次GC回收的次数太多了,STW一多,停顿时间太长,使用G1可以指定每次停顿的时间来回收一部分Region,这样就很合适。从上面停顿时间太长这个角度出发,G1就适合要求低延时的业务。
另外G1压缩内存空间有优势,适合会产生大量碎片的应用。
ParNew+CMS适合内存小的
2、总结
(1)G1小结
G1和ParNew+CMS的调优原则都是尽可能Minor GC,G1则更加智能,而PN+CMS更纯粹更直接,虽然G1在GC时没有碎片,但是由于每个Region有一个存活率大于85%不清理的机制,会导致内存没有充分释放。因此,对于cpu性能高的,内存容量大的,对应用响应度高的系统推荐使用g1。 而内存小,cpu性能比较低下的系统也可以使用pn+cms会更合适。
(2)回收过程小结
- 如果新生代未达60%,老年代未达45%,系统照常运行,不会触发回收
- 如果新生代达60%,此时如果有新对象生成,跑到新生代,就会触发Minor GC
- 开启了空间担保机制,Minor GC前先判断是否需要Full GC,如果每次回收后对象小于老年代空闲大小,则不用Full,否则要。(JDK 1.6之前是把空间担保机制和
HandlerPromotionFailure
参数拆开了,JDK 1.6之后的空间担保机制只要满足"老年代可用连续空间>新生代对象总大小或历次晋升到老年代对象的平均大小"其中一个就可,不满足就Full GC) - 不用触发Full GC,但Minor GC后的对象大于老年代空闲大小,无法直接进入老年代,触发Full GC
- 老年代堆内存占了45%了,触发混合回收(四个阶段:先STW通过GC Root初始标记哪些是有直接引用的,然后并发标记追踪GC Roots所有对象,此时与系统并发执行,接着最终标记,STW,标记并发标记过程中心新来的对象和新产生的垃圾,最后混合回收,采用的是复制算法,不会产生垃圾碎片,G1按照我们给定时间去进行性价比高的回收,回收次数可以设置,默认是八次,如果回收过程中,空闲Region超过了堆内存的5%,会提前结束,当然可以修改这个参数,另外如果回收失败,转而使用Serial Old回收器,回收变得很慢)
- 开启了空间担保机制,Minor GC前先判断是否需要Full GC,如果每次回收后对象小于老年代空闲大小,则不用Full,否则要。(JDK 1.6之前是把空间担保机制和