先通过一张完整的图来回顾CMS工作的逻辑:
CMS的缺点分析
CMS是一款优秀的收集器, 它最主要的优点在名字上已经体现出来: 并发收集、 低停顿, 一些官方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector) 。 CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试, 但是它还远达不到完美的程度, 至少有以下三个明显的缺点:
1.并发导致CPU资源紧张
首先, CMS收集器对处理器资源非常敏感。 事实上, 面向并发设计的程序都对处理器资源比较敏感。 在并发阶段, 它虽然不会导致用户线程停顿, 但却会因为占用了一部分线程(或者说处理器的计算能力) 而导致应用程序变慢, 降低总吞吐量。
CMS默认启动的回收线程数是(处理器核心数量+3) /4, 也就是说, 如果处理器核心数在四个或以上, 并发回收时垃圾收集线程只占用不超过25%的处理器运算资源, 并且会随着处理器核心数量的增加而下降。 但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。 如果应用本来的处理器负载就很高, 还要分出一半的运算能力去执行收集器线程, 就可能导致用户程序的执行速度忽然大幅降低。
比如我们常见的机器是2核4G,那么分配给CMS的回收线程数= (2+3)/4 =1 个,直接占据了一半的CPU资源
因此CMS带来的第一个问题就是影响CPU的资源使用,特别是在本身CPU核数就少的情况下。
2.Con-current Mode Failure问题
由于CMS收集器无法处理“浮动垃圾”(Floating Garbage) , 有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
在CMS的并发标记和并发清理阶段, 用户线程是还在继续运行的, 程序在运行自然就还会伴随有新的垃圾对象不断产生, 但这一部分垃圾对象是出现在标记过程结束以后, CMS无法在当次收集中处理掉它们, 只好留待下一次垃圾收集时再清理掉。 这一部分垃圾就称为“浮动垃圾”。
由于在垃圾收集阶段用户线程还需要持续运行, 那就还需要预留足够内存空间提供给用户线程使用, 因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集, 必须预留一部分空间供并发收集时的程序运作使用。
在JDK5的默认设置下, CMS收集器当老年代使用了68%的空间后就会被激活, 这是一个偏保守的设置, 如果在实际应用中老年代增长并不是太快, 可以适当调高参数-XX: CMSInitiatingOccu-pancyFraction的值来提高CMS的触发百分比, 降低内存回收频率, 获取更好的性能。 到了JDK 6时, CMS收集器的启动阈值就已经默认提升至92%。
但这又会更容易面临另一种风险: 要是CMS运行期间预留的内存无法满足程序分配新对象的需要, 就会出现一次“并发失败”(Concurrent Mode Failure) , 这时候虚拟机将不得不启动后备预案: 冻结用户线程的执行, 临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。 所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量的并发失败产生, 性能反而降低, 用户应在生产环境中根据实际应用情况来权衡设置。
3.内存碎片问题
最后一个缺点, 在本节的开头曾提到, CMS是一款基于“标记-清除”算法实现的收集器, 如果读者对前面这部分介绍还有印象的话, 就可能想到这意味着收集结束时会有大量空间碎片产生。 (如下红圈所示)
空间碎片过多时, 将会给大对象分配带来很大麻烦, 往往会出现老年代还有很多剩余空间, 但就是无法找到足够大的连续空间来分配当前对象, 而不得不提前触发一次Full GC的情况。
为了解决这个问题,CMS收集器提供了一个-XX: +UseCMS-CompactAtFullCollection开关参数(默认是开启的, 此参数从JDK 9开始废弃) , 用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程, 由于这个
内存整理必须移动存活对象, (在Shenandoah和ZGC出现前) 是无法并发的。 这样空间碎片问题是解
决了, 但停顿时间又会变长, 因此虚拟机设计者们还提供了另外一个参数-XX: CMSFullGCsBeforeCompaction(此参数从JDK 9开始废弃) , 这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定) 不整理空间的Full GC之后, 下一次进入Full GC前会先进行碎片整理(默认值为0, 表
示每次进入Full GC时都进行碎片整理) 。