美文网首页
JVM之G1垃圾回收器

JVM之G1垃圾回收器

作者: 为爱放弃一切 | 来源:发表于2020-08-05 19:30 被阅读0次

    G1垃圾回收器

    \color{red}{ParNew + CMS的组合让我们有哪些痛点?}
    stop the world,这个是最痛的一个点!无论是新生代垃圾回收,还是老年代垃圾回收,都会或多或少产生“stop the world”现象,对系统的运行时有一定影响的。所以其实之后对垃圾回收器的优化,都是朝着减少“stop the world”的目标去做的。在这个基础上,G1垃圾回收器就应运而生,它可以提供比\color{green}{ParNew + CMS}组合更好的垃圾回收的性能。

    G1垃圾回收器是可以同时回收新生代和老年代的对象的,不需要两个垃圾回收器配合起来运作,它一个人就可以搞定所有的垃圾回收。它最大的一个特点,就是把java堆内存拆分为多个大小相等的Region,如下图:

    G1.png
    但是G1仍然有新生代和老年代的概念,只不过是逻辑上的概念。也就是说,新生代可能包含了某些Region,老年代可能包含了某些Region,如下图:
    G1-Region.jpg
    G1最大的一个特点就是可以让我们设置。也就是说我们可以指定G1在垃圾回收的时候,可以保证在1小时内由G1垃圾回收导致的“stop the world”时间,也就是系统停顿的时间,不能超过1分钟。这样就相当于我们可以直接控制垃圾回收对系统性能的影响。

    \color{green}{G1是如何做到对垃圾回收导致的系统停顿可控的?}
    G1如果要做到这一点,它就必须要追踪每个Region里的回收价值,什么叫做回收价值?它必须搞清楚每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾。

    简单来说,G1可以做到让你来设定垃圾回收对系统的影响,它自己通过把内存拆分为大量小Region,以及追踪每个Region中可以回收的对象大小和预估时间,最后在垃圾回收的时候,尽量把垃圾回收对系统造成的影响控制在你指定的时间范围内,同时在有限的时间内尽量回收尽可能多的垃圾对象,这就是G1的核心设计思路

    \color{green}{Region可能属于新生代也可能属于老年代}
    刚开始Region可能谁都不属于,然后就分配给了新生代,放了很多属于新生代的对象,接着触发了垃圾回收这个Region,然后下一次这个Region可能又被分配给了老年代,用来放老年代长期存活的对象。所以G1对应的内存模型中,Region随时会属于新生代,也会属于老年代,所以没有所谓新生代给多少内存,老年代给多少内存这一说了。实际上新生代和老年代各自的内存区域是不停的变动的,由G1自动控制。

    \color{green}{如何设定G1对应的内存大小}
    我们现在来思考两个问题:
    1、到底有多少个Region?
    2、每个Region的大小是多大呢?
    其实默认情况下是自动计算和设置的,我们可以给整个堆内存设置一个大小,比如说用“-Xms”和“-Xmx”来设置堆内存的大小。然后jvm启动的时候一旦发现你使用的是G1垃圾回收器,可以使用“-XX:UserG1GC”来指定使用G1垃圾回收器,此时会自动用堆大小除以2048,因为jvm最多可以有2048个Region,然后Region的大小必须是2的倍数,比如说2MB、4MB之类的。大概就是这样子来决定Region的数量和大小的,大家一般保持默认的计算方式就可以。如果通过手动方式来指定,则可以通过“-XX:G1HeapRegionSize”参数来设置。

    Region1.jpg

    刚开始的时候,默认新生代堆内存的占比是5%,也就是占据200MB左右的内存,对应大概是100个Region,这个可以通过“-XX:G1NewSizePercent”来设置新生代的初始占比,其实维持这个默认值即可。因为在系统运行中,jvm其实会不停的给新生代增加更多的Region,但是新生代的占比最多不会超过60%,但是可以通过“-XX:G1MaxNewSizePercent”参数来调整比例。而且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少,这些其实都是动态的。

    \color{green}{新生代还有Eden和Survivor的概念吗?}
    其实在G1中虽然把内存划分为了很多的Region,但还是有新生代、老年代的区分。而且新生代里还是有Eden和Survivor的划分的,所以大家会发现之前学习的很多技术原理在G1时期都是有用的。大家应该还记得之前说过的一个新生代的参数,“-XX:SurvivorRatio=8”,所以这里还是可以区分出来属于新生代的Region里哪些属于Eden区,哪些属于Survivor区。比如之前说新生代刚开始的时候,有100个Region,那么可能80个Region就是Eden区,两个Survivor区各自占10个Region。因为新生代的Region数量是动态的,所以随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加。

    G1的新生代垃圾回收

    既然G1的新生代也有Eden和Survivor的区分,那么触发垃圾回收的机制都是类似的。随着不停的在新生代的Eden区对应的Region中放对象,jvm就会不停的给新生代加入更多的Region,直到新生代占据堆大小的最大比例60%为止。一旦新生代达到了设定的占据堆内存的最大大小60%,这个时候就会触发新生代的GC,G1就会用之前说过的复制算法来进行垃圾回收,进入一个“stop the world”状态,然后把Eden区对应的Region中的存活对象放入S1区对应的Region中,接着回收掉Eden区对应的Region中的垃圾对象。\color{green}{但是这个过程跟之前是有区别的},因为G1是可以设定目标GC停顿时间的,也就是G1执行GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills”参数来设定,默认值是200ms,那么在程序运行期间G1会根据你设定的gc停顿时间给新生代不停分配Region,然后到一定程度,就会触发新生代gc,保证新生代gc的时候导致的系统停顿时间在你预设的范围内,当然这个数字并不是那么的精准。

    \color{green}{对象什么时候进入老年代?}
    我们都知道,在G1的内存模型下,新生代和老年代各自都会占据一定的Region,老年代也会有自己的Region。按照默认新生代最大只能占据堆内存60%的Region来推算,老年代最多可以占据40%的Region,大概就是800个左右的Region。\color{blue}{那么对象什么时候从新生代进入老年代呢?}
    可以说跟之前几乎是一样的,还是这么几个条件:
    1、对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄了它就会进入老年代,“-XX:MaxTenuringThreshold”参数可以设置这个年龄。
    2、动态年龄判定规则,如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%,此时就会判断一下,比如年龄为1岁,2岁,3岁,4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象全部会进入老年代,这就是动态年龄判定规则。

    \color{red}{那之前说的大对象呢?}
    以前说是那种大对象也是可以直接进入老年代的,那么现在在G1这套内存模型下呢?实际上这里会有所改变,G1提供了专门的Region来存放大对象,而不是让大对象进入老年代中的Region。在G1中,大对象的判断规则就是一个大对象超过了一个Region大小的50%,而且一个大对象如果太大,可能会横跨多个Region来存放。\color{green}{那堆内存里哪些Region用来存放大对象呢?}不是说60%给新生代,40%给老年代吗,那还有Region给大对象?我们现在知道,在G1里,新生代和老年代的Region是不停变化的。比如新生代现在占据了1200个Region,但是一次垃圾回收之后,就让里面1000个Region都空了,此时那1000个Region就可以不属于新生代了,里面很多Region可以用来存放大对象。那么大对象既然不属于新生代和老年代,那么什么时候会触发垃圾回收呢?其实新生、老年代在回收的时候,会顺带带着大对象Region一起回收,所以,这就是在G1内存模型下对大对象的分配和回收的策略。

    \color{green}{什么时候触发新生代+老年代的混合垃圾回收?}
    G1有一个参数是“-XX:InitiatingHeapOccupancyPercent”,它的默认值是45%,意思是说,如果老年代占据了堆内存的45%的Region的时候,此时会尝试触发一次新生代+老年代一起回收的混合回收阶段。

    G1垃圾回收的过程

    \color{green}{首先会触发一个“初始标记”的操作},这个过程是需要进入“stop the world”的,但这个过程仅仅只是标记一下GC Roots直接能引用的对象,所以速度是很快的。\color{green}{接着会进入“并发标记”的阶段},这个阶段会允许系统程序运行,同时进行GC Roots追踪,从GC Roots开始追踪所有的存活对象,这个过程加入了对间接引用对象的追踪,这个过程前面的文章已经介绍过了,这里就不再赘述。\color{green}{接着下一个是“重新标记”阶段},这个阶段会进入“stop the world”,系统程序会禁止运行,最终标记一下有哪些是存活对象,有哪些是垃圾对象。\color{green}{最后一个是“混合回收”阶段},这个阶段会计算老年代中每个Region中的存活对象数量,存活对象占比,还有执行垃圾回收的预期性能和效率。接着会停止系统程序,然后全力以赴进行垃圾回收,此时会选择部分Region进行回收,因为必须让垃圾回收的停顿时间控制在我们指定的范围内,所以说它会从新生代、老年代、大对象里各自挑选一些Region,保证用指定的时间回收尽可能多的垃圾,这就是所谓的混合回收。

    G1垃圾回收器的一些参数
    最后一个阶段混合回收的时候,会停止所有程序运行,所以说G1是允许执行多次混合回收。比如先停止工作,执行一次混合回收,回收掉一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收,回收掉一些Region。有一些参数可以控制这个,比如“-XX:G1MixedGCCountTarget”参数,就是一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次。这样的好处是不让系统停止的时间过长。还有一个参数就是“-XX:G1HeapWastePercent”,默认值是5%,它的意思是说,在回收的过程中会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
    G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后再进行内存碎片的整理。
    还有一个参数,“-XX:G1MixedGCLiveThresholdPercent”,它的默认值是85%,意思是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收,否则要是一个Region的存活对象多余85%,回收它也作用不大,而且还要把85%的对象都拷贝到别的Region,这个成本是很高的。

    \color{red}{回收失败的Full GC}
    万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发一次失败。一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢的。

    G1垃圾回收器的使用场景
    当你的系统部署在大内存机器上的时候,比如说你的机器是32核64G的机器,此时你分配给系统的内存有几十个G,新生代的Eden区可能30~40G的内存。比如类似kafka、elasticsearch之类的大数据相关系统,都是部署在大内存的机器上的。此时如果你的系统负载非常的高,比如每秒几万的访问请求到kafka、elasticsearch上去,那么可能导致你Eden区的几十G内存频繁塞满,然后要触发垃圾回收,假设1分钟会塞满一次。然后每次垃圾回收要停掉kafka、elasticsearch的运行,然后执行垃圾回收大概需要几秒钟,此时你发现,可能每过一分钟,你的系统就要卡顿几秒钟,有的请求一旦卡死几秒钟就会超时报错,此时可能会导致你的系统频繁出错。\color{green}{这个时候就要我们的G1垃圾回收器出场了}!针对G1垃圾回收器,我们可以设置每次GC的停顿时间,比如我们设置100ms,那么每次垃圾回收我们的系统最多也就停顿100ms,然后系统继续运行。G1天生就适合这种大内存机器的jvm运行。

    本文结束。

    相关文章

      网友评论

          本文标题:JVM之G1垃圾回收器

          本文链接:https://www.haomeiwen.com/subject/csedrktx.html