G1 垃圾回收解决的问题及原理

背景
1.很久以来我们都是使用的ParNew + cms的垃圾组合回收方式作为jvm的标准垃圾收集器,然而随着我们使用的jvm堆内存的增加,我们发现新生代的gc时间超过了300ms,cms停顿时间也达到了300ms,由于cms的次数一般只是一天一次,所以年老代的gc我们勉强能够接受,然而由于新生代的gc几乎是每分钟一次,所以这个超时时间使我们不能够接受的,所以G1垃圾回收期就进入了我们的视野中
2.我们应用中有很多的大内存对象,寿命都是5分钟到1个小时左右,这些对象一般都会进入到年老代中,由于年老代的对象只能通过cms回收来清除,所以会造成我们的年老代对象内存上升较快,而G1有默认的参数配置:G1ReclaimDeadHumongousObjectsAtYoungGC=true,可以在新生代gc的时候回收巨型对象.
3.cms的gc是一种标记清除的垃圾收集算法,会产生大量的内存碎片,容易造成虽然整体上还有很多内存,但是实际上找不到一块连续的足够大的内存区域而导致的fullgc发生的情形,而g1的gc是一种标记清理算法,他会把待回收Region的存活对象拷贝到新的region中,然后清理掉旧的待回收Region,所以很少的内存碎片.

G1原理

  1. G1的内存布局是把整个堆内存划分成大概2048个Region的数量,每个Region的大小由XX:G1HeapRegionSize指定,一般为(1M – 2M – 4M --8M)不等, 每个Region都归属于Edge区,Old区,Survivor区的或者Humongous区的一种,所谓的Humongous区是指分配的大对象的内存数量超过了Region的大小的一半,内存布局大概如下:
    在这里插入图片描述

  2. 不管是CMS还是G1,为了减少查找存活对象时需要扫描的堆的大小,RSet是一个很重要的概念,正常情况下,比如查找新生代空间的存活对象的时候,需要扫描全部的Old代空间才能知道年老代中有哪些对象指向了新生代对象,这些年老代的对象是否还存活,然而有了RSet的概念,事情就好办了很多:Rset会记录有哪些外部区域的哪些对象引用了本区域的对象,比如:新生代的某个Region的Rset就会记录哪些年老代的Region对象引用了本Region的对象,Rset记录的优点就是可以在查找存活对象的时候避免扫描整个堆内存,缺点就是Rset数据结构也会占用内存空间一般占用为整个堆内存的1%到20%,所以为了RSet的内存占用,我们需要减少不必要的Rset集合,我们可以看一下以下的几种情形,哪一种才真正需要使用Rset:
    a. Region内部对象的互相引用,比如某个新生代/年老代Region内部的对象互相引用对方的情形,这种情形显然是不需要Rset来记录的,因为回收的时候本来就是会扫描这个特定的region的
    b. 新生代Region的对象指向年老代Region的对象,由于G1回收年老代的内存是会先进行一次ygc的操作,相当于扫描了一遍新生代region,扫描结束后自然就知道有哪些新生代对象指向年老代对象,所以这种情况也不需要记录Rset集合
    c.年老代Region的对象执行新生代Region的对象,为了避免ygc的时候扫描整个年老代空间,所以建立从年老代的Region执行新生代的Region的Rset集合非常有必要。
    顺带说一下Card Table, card table是Region中内存分配的最小单位,卡表可以理解成Rset的一种具体的实现方式即可

  3. Rset内存写屏障: 当我们在应用程序中修改对象引用比如A.x=y时,都会进入写屏障并执行一些额外的修改Rset集合的操作,这里又分为写前屏障和写后屏障两种,
    写前屏障是指等式左侧A.x所所执行的旧的引用比如z会丢失掉A.x对他的引用,所以z的Rset需要记录这种更新,
    写后屏障是指赋值操作后,引用y获得了A.x对他的引用,所以y的Rset也要记录这种更新

G1的三种垃圾回收模式

年轻代垃圾回收-ygc模式具体过程
阶段1: root扫描阶段, root包括jvm的全局变量,线程栈的局部变量还有年老代指向新生代的Rset中的变量等
阶段2: 更新Rset集合,由于内存的写屏障时,并没有具体完成更新Rset的操作,而只是记录了一个反应了这种操作的日志(其实是为了这里的批量更新操作,因为这样性能最高),所以这里需要批量更新Rset集合,该阶段完成后Rset就可以准确的反映出年老代对新生代对象的引用关系了
阶段3: 处理Rset集合,年老代指向的新生代的Rset集合中记录了Edge区的对象存活情况,这些对象都是存活的对象
阶段4: 对象拷贝操作,把新生代中存活的对象拷贝到toSurvivor区或者年老代中
阶段5:引用处理阶段,处理虚引用,弱引用以及幽灵引用等,该阶段处理完成之后,ygc工作就结束了

g1 混合gc回收模式
mixed gc回收可以分成两大步骤
步骤一:全局并发标识阶段 这个步骤的主要目的是找到要回收的old region列表,按照每个区域region的存活对象的占比排好序
步骤二: 拷贝存活对象(evacuation)阶段,这个步骤是回收上一个步骤中确定好的region,这里会按照时间的要求,分成几次回收来实现(分解的次数由XX:G1MixedGCCountTarget 参数)确定

全局并发标识阶段
阶段1:初始标识阶段(stw),当年老代的阈值达到(XX:InitiatingHeapOccupancyPercent)指定的阈值时,就会借助下一次的ygc完成该阶段,该阶段主要是找到root对象,包括jvm全局变量,线程栈局部变量,survivor中指向年老代的对象变量等(注意,这里的root变量仍然是新生代中的对象)
阶段2: 并发标识阶段(和应用线程并发执行),标识年老代中根对象,也就是全局变量执行的年老代对象,survivor指向的年老代中的年老代对象,把这些年老代对象也表示为sub root的一部分.(和阶段1的区别是这次标识sub root对象都是年老代中的对象)
阶段3: 并发标识阶段(和应用线程并发执行),这个阶段会对堆中的对象的进行可达性分析,标识存活的对象,并且这个过程中应用线程产生的新应用或者应用的更新也会被写屏障记录下来,这个阶段中会标识每个区域的对象存活比例。
阶段4:重新标识阶段(stw),这个阶段会处理并发标识阶段中由于新应用和引用变更引起的变动,找到那些没有被标识的存活对象
阶段5:清理阶段(stw),这个阶段会根据暂停时间目标和成本进行Cset回收集合计划的制定(注意这里主要只是制定计划,而不会进行实际的垃圾回收),该阶段中会识别出垃圾占比高的old region,并把该区域加入到待回收的Cset集合中去,此外,如果某些old region已经完全没有存活的对象,那么在该阶段中也会顺便把这些old region回收掉,而不用再加入Cset集合中了

步骤二:拷贝存活对象
步骤一全局并发标识结束后,已经把待回收的region确定好了,g1 只需要在恰当的时机进行回收就可以了。
(a) 这里基于暂停时间的考虑,混合回收会分成多次进行(由参数XX:G1MixedGCCountTarget 指定,默认8次),相当于每一次会收集1/8的old region,edge 区域以及survivor区域.
( b ) 包含垃圾的old region是否会被加入到待回收的Cset集合中主要由参数XX:G1MixedGCLiveThresholdPercent决定,该参数的默认值为65%,意味着只有包含垃圾的占比超过65%的old region才会被垃圾收集
© 混合gc的次数也不一定要进行8次,可以提前结束gc收集,原因是后面收集到的垃圾数量太少,需要复制的存活对象太多,耗时很长,收益太低,而这个是否可以提前结束gc收集的参数可以有XX:G1HeapWastePercent参数决定,默认10%,意味着如果可以回收到的垃圾数量占比低于10%,那么进行玩本次垃圾回收后就停止gc收集

Full GC 垃圾回收
full gc是一种单线程的,stw式的标记整理的垃圾收集算法(注意这里从jdk10开始,full gc已经是多线程stw式的并行回收),该过程及其影响应用程序的运行,以下几种情况下会导致gc进入这种gc收集状态:
(a) 从年轻代拷贝对象到空闲空间时,找不到足够的空闲空间
(b)从年老代拷贝对象到空闲空间时,找不到足够的空闲空间
© 分配大对象时找不到连续的足够的空闲存放大对象
Full gc是一种应用程序应该极力避免的gc回收方式:
(a)可以通过增大参数XX:G1ReservePercent的值,默认10%,通过保留更多的空闲空间来应对内存的需求.
(b) 可以通过调整XX:InitiatingHeapOccupancyPercent提前触发全局并发标识的方式提前触发mix gc操作,提前回收内存空间

重要参数汇总:

  1. 对于G1来说,一般情况下绝对不能设置-xmn和-xx:NewRatio的方式设置新生代的大小,因为G1会动态的调整新生代的大小来使得ygc的时间达到预期的时间内,所以一般只会通过如下设置新生代的内存使用范围:XX:G1NewSizePercent XX:G1MaxNewSizePercent
  2. -Xms = -Xmx -XX:+AlwaysPreTouch 这三个参数一般肯定是要设置的,含义就是设置堆的初始和最大大小,并且在启动jvm的时候就像操作系统申请好了jvm使用的内存了
  3. 如果mixed gc的耗时太长,可以调整XX:G1MixedGCCountTarget参数增大回收的次数,分摊每次回收的区域大小,或者增大XX:G1MixedGCLiveThresholdPercent的值,只回收垃圾占比高的old region,因为垃圾占比低的old region的复制操作耗费很长的时间,或者增加XX:G1HeapWastePercent的值,如果本次回收到的内存从比例小于这个值的话,就不在进行gc收集操作了
  4. 如果在扫描root对象阶段耗时很长,那么原因一般是由于Rset集合数量太多引起的,所以我们可以通过增加XX:G1HeapRegionSize 参数加大每个region的大小,这样跨region的引用就会减少了。Rset的数量也会相应的减少了.
  5. XX:ParallelGCThreads stw时并行的回收的gc线程的个数,XX:ConcGCThreads和应用程序线程并发执行的gc线程的个数,如果需要提高gc回收的速度,可以试着提高这两个值.
  6. 如果日志中发现Pause Full也就是发生了fullgc,那么可以调整XX:G1ReservePercent参数,增加预留的空闲空间的百分比,调大这个参数可以降低自适应IHOP的值,或者也可以直接关闭自适应IHOP的值而采用手工指定的方式,只需要设置以下两个参数即可-XX:-G1UseAdaptiveIHOP 和XX:InitiatingHeapOccupancyPercent

参考文献:
https://blog.csdn.net/a745233700/article/details/121724998
https://docs.oracle.com/en/java/javase/18/gctuning/garbage-first-garbage-collector-tuning.html#GUID-A0343B53-A690-4DDE-98F9-9877096DBF0F
https://www.oracle.com/cn/technical-resources/articles/java/g1gc.html

G1(Garbage-First)垃圾回收器是一种面向服务端应用的垃圾回收器,它的工作原理如下: 1. 初始标记(Initial Mark):在此阶段,G1会暂停所有应用线程,标记所有根对象,并记录下所有从根对象直接可达的对象。 2. 并发标记(Concurrent Marking):在此阶段,G1会与应用线程并发工作,标记所有从根对象间接可达的对象。这个过程是增量进行的,即与应用线程交替执行,以减少对应用程序的停顿时间。 3. 最终标记(Final Mark):在此阶段,G1会再次暂停所有应用线程,完成并发标记阶段中未完成的对象标记。 4. 筛选回收(Live Data Counting and Evacuation):在此阶段,G1会根据堆中各个区域的回收价值进行筛选,确定哪些区域中的对象将被回收。然后,G1会将存活对象从这些区域中复制到空闲区域。 5. 复制(Humongous Copy):在此阶段,G1会将大对象(Humongous Objects)从一个区域复制到另一个区域。大对象是指超过一定阈值的对象,它们可能会导致碎片问题G1通过复制来解决这个问题。 6. 清理(Cleanup):在此阶段,G1会清理已经被复制的区域,回收其中的未存活对象,使这些区域变为空闲状态。 7. 筛选回收(Live Data Counting and Evacuation):G1会再次进行筛选回收,重复步骤4和5,直到达到预设的回收目标。 G1垃圾回收器通过将堆内存分成多个大小相等的区域(Region),以增量的方式进行垃圾回收,从而减少应用程序的停顿时间,并且能够根据实际情况动态调整每个区域的大小,以适应不同应用的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值