Java教程:9种常见的CMS GC问题详解(十一)

开课吧开课吧锤锤2021-03-24 10:58

    Java编程语言是一种简单、面向对象、分布式、解释型、健壮安全、与系统无关、可移植、高性能、多线程和动态的语言。如今Java已经广泛应用于各个领域的编程开发。

Java

    4.7场景七:内存碎片&收集器退化

    4.7.1现象

    并发的CMSGC算法,退化为Foreground单线程串行GC模式,STW时间超长,有时会长达十几秒。其中CMS收集器退化后单线程串行GC算法有两种:

    带压缩动作的算法,称为MSC,上面我们介绍过,使用标记-清理-压缩,单线程全暂停的方式,对整个堆进行垃圾收集,也就是真正意义上的FullGC,暂停时间要长于普通CMS。

    不带压缩动作的算法,收集Old区,和普通的CMS算法比较相似,暂停时间相对MSC算法短一些。

    4.7.2原因

    CMS发生收集器退化主要有以下几种情况:

    晋升失败(PromotionFailed)

    顾名思义,晋升失败就是指在进行YoungGC时,Survivor放不下,对象只能放入Old,但此时Old也放不下。直觉上乍一看这种情况可能会经常发生,但其实因为有concurrentMarkSweepThread和担保机制的存在,发生的条件是很苛刻的,除非是短时间将Old区的剩余空间迅速填满,例如上文中说的动态年龄判断导致的过早晋升(见下文的增量收集担保失败)。另外还有一种情况就是内存碎片导致的PromotionFailed,YoungGC以为Old有足够的空间,结果到分配时,晋级的大对象找不到连续的空间存放。

    使用CMS作为GC收集器时,运行过一段时间的Old区如下图所示,清除算法导致内存出现多段的不连续,出现大量的内存碎片。

Java

    碎片带来了两个问题:

    空间分配效率较低:上文已经提到过,如果是连续的空间JVM可以通过使用pointerbumping的方式来分配,而对于这种有大量碎片的空闲链表则需要逐个访问freelist中的项来访问,查找可以存放新建对象的地址。

    空间利用效率变低:Young区晋升的对象大小大于了连续空间的大小,那么将会触发PromotionFailed,即使整个Old区的容量是足够的,但由于其不连续,也无法存放新对象,也就是本文所说的问题。

    增量收集担保失败

    分配内存失败后,会判断统计得到的YoungGC晋升到Old的平均大小,以及当前Young区已使用的大小也就是最大可能晋升的对象大小,是否大于Old区的剩余空间。只要CMS的剩余空间比前两者的任意一者大,CMS就认为晋升还是安全的,反之,则代表不安全,不进行YoungGC,直接触发FullGC。

    显式GC

    这种情况参见场景二。

    并发模式失败(ConcurrentModeFailure)

    最后一种情况,也是发生概率较高的一种,在GC日志中经常能看到ConcurrentModeFailure关键字。这种是由于并发BackgroundCMSGC正在执行,同时又有YoungGC晋升的对象要放入到了Old区中,而此时Old区空间不足造成的。

    为什么CMSGC正在执行还会导致收集器退化呢?主要是由于CMS无法处理浮动垃圾(FloatingGarbage)引起的。CMS的并发清理阶段,Mutator还在运行,因此不断有新的垃圾产生,而这些垃圾不在这次清理标记的范畴里,无法在本次GC被清除掉,这些就是浮动垃圾,除此之外在Remark之前那些断开引用脱离了读写屏障控制的对象也算浮动垃圾。所以Old区回收的阈值不能太高,否则预留的内存空间很可能不够,从而导致ConcurrentModeFailure发生。

    4.7.3策略

    分析到具体原因后,我们就可以针对性解决了,具体思路还是从根因出发,具体解决策略:

    内存碎片:通过配置-XX:UseCMSCompactAtFullCollection=true来控制FullGC的过程中是否进行空间的整理(默认开启,注意是FullGC,不是普通CMSGC),以及-XX:CMSFullGCsBeforeCompaction=n来控制多少次FullGC后进行一次压缩。

    增量收集:降低触发CMSGC的阈值,即参数-XX:CMSInitiatingOccupancyFraction的值,让CMSGC尽早执行,以保证有足够的连续空间,也减少Old区空间的使用大小,另外需要使用-XX:+UseCMSInitiatingOccupancyOnly来配合使用,不然JVM仅在第一次使用设定值,后续则自动调整。

    浮动垃圾:视情况控制每次晋升对象的大小,或者缩短每次CMSGC的时间,必要时可调节NewRatio的值。另外就是使用-XX:+CMSScavengeBeforeRemark在过程中提前触发一次YoungGC,防止后续晋升过多对象。

    4.7.4小结

    正常情况下触发并发模式的CMSGC,停顿非常短,对业务影响很小,但CMSGC退化后,影响会非常大,建议发现一次后就彻底根治。只要能定位到内存碎片、浮动垃圾、增量收集相关等具体产生原因,还是比较好解决的,关于内存碎片这块,如果-XX:CMSFullGCsBeforeCompaction的值不好选取的话,可以使用-XX:PrintFLSStatistics来观察内存碎片率情况,然后再设置具体的值。

    最后就是在编码的时候也要避免需要连续地址空间的大对象的产生,如过长的字符串,用于存放附件、序列化或反序列化的byte数组等,还有就是过早晋升问题尽量在爆发问题前就避免掉。

    以上内容由开课吧老师、新宇、湘铭、祥璞提供,更多Java教程尽在开课吧广场Java教程频道。

有用
分享