在G1出来之前,CMS绝对是OLTP系统的标配。即使G1出来⼏年了,⽣产环境很多的JVM实例还是采⽤ParNew+CMS的组合。但是即使其得到这么⼴泛的应⽤,还是有很多同学对它有很深的误解。本⽂主要对ParNew+CMS经典组合下,触发的⼏种垃圾回收⽅式进⾏⼏个概念的纠正。
Backgroud CMS
可能更多⼈只知道CMS,⽽不知道Backgroud CMS。事实上我们说的CMS,即包含了5个阶段的CMS,就是Background CMS,如下图所⽰:
说明:
- 图中初始化标记阶段是串⾏的,这是JDK7的⾏为。JDK8以后默认是并⾏的,可以通过参数
-XX:+CMSParallelInitialMarkEnabled
控制。 - 由图可知,CMS还有两个阶段是完全STW(Stop The World)的,即初始化标记和最终标记(重新标记)。
- 其他阶段都是并发的,所以CMS被称为
Concurrent Mark&Sweep
,但是我认为前⾯还需要加个Mostly才是最贴切,即CMS是⼀个Mostly Concurrent Mark and Sweep Garbage Collector
,因为它还没办法做到完全并发。不只是CMS,就是G1,以及JDK11的ZGC都没有做到完全的并发。就⽬前笔者了解到的所有GC中,只有Azul的C4是完全并发的。
为什么有个Background关键词?我们都知道配置CMS垃圾回收的话,有两个重要参数:-XX:CMSInitiatingOccupancyFraction=75`` -XX:+UseCMSInitiatingOccupancyOnly
,这两个参数表⽰只有在Old区占了75%的内存时才满⾜触发CMS的条件。注意这只是满⾜触发CMS GC的条件。⾄于什么时候真正触发CMS GC,由⼀个后台扫描线程决定。CMSThread默认2秒钟扫描⼀次,判断是否需要触发CMS,这个参数可以更改这个扫描时间间隔,例如-XX:CMSWaitDuration=5000,此外可以通过jstack⽇志看到这个线程:
:::info
“Concurrent Mark-Sweep GC Thread” os_prio=2 tid=0x000000001870f800 nid=0x0f4 waiting on condition
:::
Foregroud CMS
这个名词第⼀次听笨神说的(公众号:你假笨)。当然笨神也不是随便⾃⼰捏造⼀个名词出来,这个名词来⾃于openjdk源码,参
考:concurrentMarkSweepGeneration.cpp
它发⽣的场景,⽐如业务线程请求分配内存,但是内存不够了,于是可能触发⼀次CMS GC,这个过程就必须要等待内存分配成功后业务线程才能继续往下⾯⾛,因此整个过程必须STW,所以这种CMS GC整个过程都是STW,但是为了提⾼效率,它并不是每个阶段都会⾛的,只⾛其中⼀些阶段,通过上⾯的源码可知,这些省下来的阶段主要是并⾏阶段:Precleaning、AbortablePreclean,Resizing。但不管怎么说如果⾛了类似foreground这种CMS GC,那么整个过程业务线程都是不可⽤的,效率会影响挺⼤。
这事实上就是发⽣了FullGC,由这段的分析可知FullGC相⽐CMS Backgroud collect模式差距还是⾮常⼤的。
MSC
MSC的全称是Mark Sweep Compact,即标记-清理-压缩,MSC是⼀种算法,请注意Compact,即它会压缩整理堆,这⼀点很重要。
这是foreground CMS在特定情况下才会采⽤的⼀种垃圾回收算法。为什么这么说了,这⾥需要介绍两个参数,这两个参数表⽰多少次FullGC后采⽤MSC算法压缩堆内存,0表⽰每次FullGC后都会压缩,同时0也是默认值。
:::info
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
:::
配置-XX:+UseCMSCompactAtFullCollection(默认)前提下,如果CMSFullGCsBeforeCompaction=0,那么每次foreground CMS后都
会采⽤MSC算法压缩堆内存;如果CMSFullGCsBeforeCompaction=3,那么每3次foreground CMS后才会有1次采⽤MSC算法压缩堆内
存。
碎⽚问题也是CMS采⽤的标记清理算法最让⼈诟病的地⽅:Backgroud CMS采⽤的标记清理算法会导致内存碎⽚问题,从⽽埋下发⽣FullGC导致长时间STW的隐患。
所以如果触发了FullGC,⽆论是否会采⽤MSC算法压缩堆,那都是ParNew+CMS组合⾮常糟糕的情况。因为这个时候并发模式已经搞不定了,⽽且整个过程单线程,完全STW,可能会压缩堆(是否压缩堆通过上⾯两个参数控制),真的不能再糟糕了!想象如果这时候业务量⽐较⼤,由于FullGC导致服务完全暂停⼏秒钟,甚⾄上10秒,对⽤户体验影响得多⼤。
另外,别以为G1就好很多,G1的FullGC同样是垃圾级别的存在:
The G1 garbage collector is designed to avoid full collections, but when the concurrent collections can’t reclaim memory fast enough a fall back full GC will occur. The current implementation of the full GC for G1 uses a single threaded mark-sweep-compact algorithm.
HOW?
FullGC这么恐怖,有办法缓解么,或者说尽量避免它在⽩天,甚⾄业务⾼峰期出现?有!笔者给你分享⼀个歪门邪道,不记得是多少年前,在哪⾥道听途说才得到这个偏⽅的,⽽且据说以前阿⾥的⼀些业务也⽤了这个偏⽅,不管是哪⾥得来的偏⽅,反正肯定有⽤的。这个偏⽅很简单:在业务最低峰期(⽐如⼤陆的很多业务可以选在凌晨2,3点夜深⼈静的时候)强⾏触发FullGC(需要结合参数-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
,这两个参数默认值就是这样的,表⽰触发FullGC时压缩堆),从⽽优化内存碎⽚并压缩堆,降低在业务⾼峰期发⽣FullGC的概率(只能降低,不能杜绝)。
强⾏触发FullGC: :::info
没有开启-XX:+DisableExplicitGC的前提下调⽤System.gc()就会发⽣FullGC
System.gc();
或者通过jmap命令触发:
# jmap -histo:live pid
总结
按照惯例,最后来个总结:
- 正常情况下触发Backgroud模式的CMS GC,这是并发模式收集,对业务影响很⼩。
- 当并发模式搞不定了,就会退化成Foreground模式,这个回收过程业务线程是不可⽤的,这时候就触发了FullGC。
- 接下来根据上⾯提到的两个参数决定是否采⽤MSC算法压缩堆。
- CMSFullGCsBeforeCompaction决定多少次FullGC后压缩堆,具体配置多⼤,由你决定,但是不建议太⼤,否则在采⽤MSC算法压缩堆之前,由于内存碎⽚的问题,导致出现promotion failure,总之这是trade-off。