JVM学习笔记(7):G1垃圾回收器详解与回收性能优化

G1垃圾回收器详解与回收性能优化

一、G1的引出

1、G1与堆内存

  新生代还是老年代,Stop the World是最大的痛点,它们都会产生这个现象,影响系统的运行,所有垃圾回收器的优化都是朝着减少STW的目标去做的,G1便应运而生了。G1可以同时回收新生代和老年代,它的最大特点,就是把Java堆内存拆分为多个大小相等的Region,如图,所以G1的新生代老年代是一种逻辑上的概念了,新生代可能包含了某些Region,老年代可能包含了某些Reigon另一个最大特点,就是允许我们设置一个垃圾回收的预期停顿时间,比如我们可以指定1小时内回收垃圾的时候产生的STW时间要小于1min

  G1相比之前的垃圾回收器,最大进步就是STW可控

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FEvznQE5-1581232833681)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191025205227363.png)]

  给整个堆内存设置了大小后,启动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个,如图)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MUkGTmiC-1581232833683)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191025220101126.png)]

这时候还是会触发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,进行扫描,标记出来他们直接引用的那些对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oKehIY34-1581232833684)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191025224910516.png)]

(2)并发标记阶段

这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,并对这个过程对象的变化做记录,比如哪些对象失去了引用,哪些对象是新建的。如下图所示。(这个阶段也是很耗时的,要追踪全部存活的对象,但跟系统并发运行,影响不大)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k9y2CwrZ-1581232833686)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191025225048722.png)]

(3)最终标记阶段

这个阶段会进入Stop the World系统程序是禁止运行的,但是会根据并发标记阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象,如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPNSOlQI-1581232833687)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191025225515257.png)]

(4)混合回收阶段

  基于复制算法,这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内。

  注意,这里到底回收哪些Region是G1自己选择的,这里的混合回收是指在我们指定的时间(比如200ms)内回收尽可能多的垃圾。

  另外,这个阶段G1允许多次执行混合回收,也就是说先停止系统工作,执行回收,恢复系统运行,再停止系统运行,再回收,再恢复…这么一个流程。每次回收的间隔是由G1自己控制的,回收执行次数可以通过参数-XX:G1MixedGCCountTarget来设置,这个参数默认回收次数是8次,同时有一个参数-XX:G1HeapWastePercent,默认值是5%,就是说空出来的区域大于整个堆的5%,就会立即停止混合回收了。正常默认回收次数是8次,但是可能到了4次,发现空闲Region大于整个堆的5%,就不会再进行后续回收了。这种多次回收的机制能够让系统停顿时间不要太长,可以在多次回收的间隙也运行一下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cCauDw0d-1581232833688)(/Users/zhangye/Library/Application Support/typora-user-images/image-20191025225705796.png)]

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)回收过程小结
  1. 如果新生代未达60%,老年代未达45%,系统照常运行,不会触发回收
  2. 如果新生代达60%,此时如果有新对象生成,跑到新生代,就会触发Minor GC
    1. 开启了空间担保机制,Minor GC前先判断是否需要Full GC,如果每次回收后对象小于老年代空闲大小,则不用Full,否则要。(JDK 1.6之前是把空间担保机制和HandlerPromotionFailure参数拆开了,JDK 1.6之后的空间担保机制只要满足"老年代可用连续空间>新生代对象总大小或历次晋升到老年代对象的平均大小"其中一个就可,不满足就Full GC)
    2. 不用触发Full GC,但Minor GC后的对象大于老年代空闲大小,无法直接进入老年代,触发Full GC
    3. 老年代堆内存占了45%了,触发混合回收(四个阶段:先STW通过GC Root初始标记哪些是有直接引用的,然后并发标记追踪GC Roots所有对象,此时与系统并发执行,接着最终标记,STW,标记并发标记过程中心新来的对象和新产生的垃圾,最后混合回收,采用的是复制算法,不会产生垃圾碎片,G1按照我们给定时间去进行性价比高的回收,回收次数可以设置,默认是八次,如果回收过程中,空闲Region超过了堆内存的5%,会提前结束,当然可以修改这个参数,另外如果回收失败,转而使用Serial Old回收器,回收变得很慢)
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值