一)G1垃圾回收器:-XX:+UseG1GC:使用G1收集器
1)垃圾收集器迭代停顿时间越少越好,但是垃圾回收的总时间会增多,默认暂停时间默认是200ms,G1的内部底层算法非常复杂比CMS复杂,如果大内存,G1还比较有效果,但是如果堆内存不大,G1性能还不如CMS,在小内存效率依然不高;
2)注重于停顿时间和回收regin的效益比,适用于堆内存比较大的内存,最大停顿时间的优势就凸显出来了,停顿时间比较短,对于用户体验非常好,适用于高并发大内存的情况下,很好的解决大内存STW时间更长的问题;
3)空白的regin使用链表来进行连接
一)G1垃圾收集器介绍
1)G1垃圾回收器是一款面向服务器的垃圾收集器,主要针对的是配备多颗处理器以及大容量内存的机器,以极高的频率来满足GC停顿时间的要求的同时来尽量的提升吞吐量
2)虽然在物理上分代已经不连续了,但是在逻辑上还是存在着分代的概念的
G1在大内存中对整个用户的停顿时间可以控制的优势是非常明显的,如果内存特别大,那么可控停顿时间的优势就会非常明显
但是在小内存中G1的优势就不算是太明显了,因为G1内部有很多很难的内部算法细节,整个算法时间复杂度非常高,又要估计regin的回收价值,又要分配卡表和记忆集,本来CMS和parNew的垃圾回收也是可以的了;
3)G1堆将JAVA的堆划分成多个大小相等的独立区域,JVM最多有2048个regin,一般的regin大小等于堆内存大小除以2048,比如说堆的大小是4096M那么regin的大小就是2M,当然也是可以使用参数-XX:G1HeapReginSize来手动指定Regin的大小,但是推荐默认的计算方式
4)G1保留了年轻代和老年代的概念,但是不在是物理隔阂了,都是不连续的Regin的集合,默认年轻代对于堆内存的占比是5%,如果堆内存大小是4096M,那么年轻代大概占据200M的内存,大概是100个regin,可以使用-XX:G1NewSizePercent来设置新生代初始占比,在系统运行过程中JVM会不断给年轻代增加更多的regin,但是最多新生代的占比也没有超过60%,可以通过-XX:G1MaxNewSizePercent调整,年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个,一开始regin内存区域都是空的,随着空间不断被使用,年轻代不在扩容,程序运行后期这些不同的regin会被赋予不同的含义;
5)一个regin可能之前是年轻代,如果regin进行了垃圾回收,可能就变成了老年代,也就是说regin的功能区域可能会动态发生变化
6)G1垃圾收集器对于对象什么时候会转移到老年代和之前说过的原则一模一样,唯一不同的是针对于大对象的处理,G1有一个专门分配大对象的区域叫做Humongous区域,而不是让大对象直接进入到老年代的Regin中,在G1中有专门分配大对象的区域叫做Humongous区,大对象的判断规则就是一个对象超过了Regin区域的一半,按照上面的计算规则,每一个Regin的大小是2M,只要对象的大小超过了1M,就会被存放到Humongous,如果对象特别大,还有可能使用多个连续的Regin存放,Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销FullGC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收;
1)G1垃圾收集器会让初始标记和最终标记和筛选回收的总的STW时间控制在一个设置的参数范围内,在筛选回收中G1垃圾回收器不一定会把所有的堆中的垃圾全部回收掉,可能在筛选回收之前并发标记过程中标记了很多非垃圾对象还有很多的垃圾对象要进行清理,但是在G1筛选回收阶段,因为要考虑到停顿时间,可能要把整个堆空间回收完成要400ms,但是程序员指定的STW时间是200ms,G1会有一个算法来预估回收多少块区域(预估每一块regin大概要回收多长时间)能够达到用户设置的最大停顿时间,剩余的区域下一次GC回收;
2)一共的GC总时间比CMS时间还长,ZGC的垃圾回收一次完整过程的时间可能会更久;
3)可不可以把GC的停顿时间指定的非常短呢,不要乱指定,既然需要停顿时间短一些,就指定为10ms,G1一次性清理的垃圾非常非常少,垃圾积累多了,可能会触发大级别的FullGC,直接STW,直接单线程垃圾收集,本来设置时间是200ms,但是你设置10ms,最终可能导致G1停顿时间失效;
4)G1垃圾回收器在后台维护了一个优先级列表,每一次根据允许的收集时间,优先进行选择回收加载最大的regin,比如说一个regin花费200ms能够回收10M垃圾,另外一个regin花费50ms能够回收20M垃圾,在总回收时间有限的情况下,G1当然会优先回收后面这个;
5)一块区域存活的对象越多,垃圾回收的效益比越低,又要移动对象更新引用啥的,应该优先回收垃圾比较多的regin区域;
二)G1垃圾回收分类:
2.1)MinorGC:
伊甸园区默认占用整个堆空间的5%,假设现在伊甸园区放满了,之前的垃圾回收器Parallel NEW,伊甸园区满了之后会触发young GC,但是对于G1来说并不是这样,原始的伊甸园区放满以后并不会触发Young GC,G1有一个最大默认停顿时间默认值是200ms,G1垃圾回收器会进行计算,假设这5%的伊甸园区如果要是做young GC的回收接近200ms,会触发minorGC,但是如果垃圾回收器计算下来整个伊甸园区计算下来只需要50ms,远远小于200ms,那么就说明此时伊甸园区太小了,垃圾其实并不多,但是G1是分区算法,5%的伊甸园区满了以后,G1垃圾回收器会把这些新new出来的对象放在其他新的空闲的空的regin格子中,那么此时这个regin的格子就是一个新的伊甸园区,此时如果隔一段时间之后发现系统评估的伊甸园区总的回收时间接近于200ms,此时就会触发youngGC,G1垃圾回收器在进行触发young GC之前会评估一下计算一下当下伊甸园区的回收时间是否接近于最大停顿时间,如果接近了,就做minor GC,如如果最大停顿时间远远大于伊甸园区的总的回收时间,那么此时伊甸园区会扩容,把这些新对象丢到新的空区域,这些新的空区域就又变成了伊甸园区,直到G1垃圾回收器判断整个伊甸园区的垃圾回收时间接近于最大停顿时间,此时才会触发minor GC;
2.2)MixedGC:
不是FullGC,老年代的堆空间的占有率达到参-XX:IniatiattingHeapingOccupanyPercent设定的值来触发,根据最大停顿时间来计算回收效益比,它会回收所有的Young和部分Old区域(根据GC的停顿时间确定Old区域垃圾收集的先后顺序)以及大对象区域,正常情况下G1的垃圾收集是先做MixedGC,主要采用的是复制算法,需要把各个Regin中的存活的对象拷贝到别的Regin区域里面去,拷贝过程中如果发现没有空的Regin能够承载拷贝对象就会触发一次FullGC,会进行回收所有的新生代和部分老年代,因为它还是要根据停顿时间来进行计算最大回收效益比,对于大对象区域的HS也会回收,
2.3)FullGC:
如果说触发MixedGC的占用老年代的参数设置的比较小比如说45%,那么55%都是新生代,如果新生代太多,说明此时新生代占用regin过多,此时就没有额外的空闲regin供老年代对象来进行复制了,此时就会触发Full gc,因为复制算法是对剩余的空间要求是比较高的,如果剩余空间太小没有充足的空间进行复制,此时没空间存放来年代对象,要对停顿时间做一个好的把控,此时直接单线程收集,使用单线程垃圾回收器进行回收的,STW时间非常长;
之前的垃圾回收器,只是需要在年轻代记录一个卡表就可以了,但是在G1来说,每一个regin区域都有一个卡表,记录着老年代区域对于新生代的引用关系;
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的,Shenandoah优化成多线程收集了;
针对于-XX:MxGCPauseMillis这个参数来说,如果说这个值设置的特别大,可能会导致系统运行很久,年轻代可能都已经占用到了堆内存的60%了,才会触发YGC,那么此时存活的对象很多,此时就有可能导致幸存区存不下那么多的对象,就会进入到老年代,或者是说年轻代GC以后,存活下来的对象过大导致进入到幸存区触发了动态年龄判断机制,达到了幸存者区的50%,也会是得一些对象快速进入到了老年代
XX:MaxGCPauseMills这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixedgc
1)CMS并发清理的过程中回收垃圾是和用户线程并发执行的,G1的筛选回收过程是会造成STW的;
2)CMS并发清理过程中会产生内存碎片,但是G1时使用的复制算法,不会产生内存碎片;
3)G1使用了Regin划分内存空间以及有优先级的区域回收方式,保证了G1在有限的垃圾回收时间内尽可能保证比较高的回收效率
三)G1的使用场景:
1)假设现在在每秒几十万数据量的高并发的系统的环境下,KFK一秒钟可以处理几十万单,肯定要使用到大内存,此时就是需要使用GC,小内存是肯定不符合要求的,假设一个订单一KB,几十万的消息,每一秒钟有5 6个G放到新生代,这个时候有大量数据对象直接晋升到老年代,等了几秒以后发现老年代也放不下,可能每几秒钟就需要做一次FullGC,但是触发fullGC可能,消息还没有执行完,此时还是放不出来空间,此时直接报OOM;
2)所以为了避免系统触发FullGC或者导致OOM,针对于这种高并发场景肯定是用大内存的机器,因为大部分高并发对象都是朝生夕死,经过一次minorGC有可能就被回收,所以将新生代的空间设置的比较大,5 6g都可以,但是此时minorGC的执行是非常慢的,会造成卡顿,这个时候做GCroot,遍历整个年轻代的对象速度很慢,尽管存活对象不多,但是要遍历几十个G的新生代还是比较浪费时间,所以young GC的回收速度不一定比老年代回收速度快,因为如果新生代特别大,就是一个反例,还有就是新生代的垃圾回收非常慢,如果不使用G1,那么此时的用户会非常卡顿,如果不使用G1那么STW时间非常长
3)如果用G1,使用固定停顿时间200ms,每一次GC也可以收集好几个G,可以回收内存空间,虽然整个GC时间比较多,但是不会导致用户等待时间非常长,虽然总的吞吐量还不错,对于用户体验还不错;
4)G1边处理业务,边收集垃圾,STW降到非常短的时间
为什么要分代?因为很多对象的生命周期不一致,有的对象朝生夕死new出来之后需要马上进行销毁掉,有的老对象,生命周期就是很短,如果将这些对象放在一起,每一次都需要扫描整个堆的区域,是很麻烦的,那堆时时刻刻都需要做GC的;
堆空间越大,垃圾回收时间会变长,用户基本无感知,那么震哥哥系统就可以在卡顿几乎无感知的情况下一便处理业务一边收集垃圾;
四)G1为什么使用增量更新?
1)增量更新效率更低,因为要从增量节点数据结构的根节点继续向下扫描,G1要是采取扫描的话,你会涉及到很多regin区域的跨代扫描,就会比较麻烦,这个时候跨很多代,遍历的复杂度会更高,增量更新不需要跨很多代,老年代有很多卡表;
2)但是CMS只有一个卡表,STAB只需要标记一下,不需要重新扫描,只需要下一轮GC再次进行回收即可,下一次再去重新去回收垃圾即可,对于G1来说,多一些浮动垃圾也没啥事,因为本身算法很多垃圾对象都不会回收,多一些浮动垃圾也没啥事;
五)G1垃圾回收器的回收过程:
1)初始标记:暂停所有的其他线程,并记录下gcroots直接能引用的对象,速度很快
2)并发标记:如果在并发标记过程中发现某些regin对象的存活率很小甚至于说基本没有对象存活,那么G1就会在这个阶段将整个regin回收
3)重新标记:
4)筛选回收:筛选回收阶段实现针对每一个的Regin的回收价值和成本来进行排序,根据用户所期望的GC停顿时间来制定回收计划,比如说此时老年代有1000个regin都满了,但是因为根据本次停顿时间,本次来及回收可能只是停顿200ms,那么根据之前回收成本计算可知回收其中的800个regin可能只是需要200ms,那么就只会回收800个regin,尽量把GC的停顿时间控制在我们想要的范围内
其实这个过程也是可以和用户线程一起并发执行,但是因为只是回收一部分regin,时间是用户可控制的,而且停顿用户线程可以提升垃圾回收的效率,不管是年轻代还是老年代,回收算法主要采用的是复制算法,将一个regin的存活对象存放到另一个regin中,这种不会像CMS一样回收玩因为还有很多内存碎片需要重新整理一次,G1采用复制算法几乎不会有太多的内存碎片
(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)
六)G1垃圾回收器的特点: