简介JVM的CMS垃圾收集器

来源:互联网 发布:python 移除文件夹 编辑:程序博客网 时间:2024/05/29 07:37

CMS(Concurrent Mark Sweep):

是一款基于“标记-清除”算法,以获取最短GC停顿时间为目标的垃圾收集器,B/S系统尤其青睐该收集器,以带给用户最优的体验。

大部分文档及书籍描述该收集器的收集过程为4个,其实看GC日志我们能知道,CMS的收集过程是分为7个步骤的:

初始标记(CMS-initial-mark --STW):标记老年代中所有的 GC Roots能直接关联到的对象,打印日志:CMS-initial-mark

并发标记(CMS-concurrent-mark): 标记老年代中所有GCRoots可达的对象;打印日志:CMS-concurrent-mark-start ,CMS-concurrent-mark

预清理(CMS-concurrent-preclean):标记引用发生了变化的对象;做一些清理工作;打印日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean

可控预清理(CMS-concurrent-abortable-preclean):可终止的CMS-concurrent-proclean;打印日志: CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,CMS:abort preclean due to time XXX

重新标记(CMS-Remark --STW):标记老年代中所有存活的对象;打印日志: YG occupancy,CMS remark

并发清除(CMS-concurrent-sweep):并发清除无用对象

重置相关数据结构(CMS-concurrent-reset):重置CMS内部数据结构,准备下一次CMS GC


CMS收集器优点:并发收集,低停顿。

缺点:

1.CPU资源敏感:默认回收线程数为(CPU数+3)/4,当CPU数多于4时,回收线程对CPU的消耗X,X>=25%&&X<=40%(这里《深入理解java虚拟机 --周志明》一书中写的是不超过25%,笔者关于这个问题和同事争论了很久,但还是坚持自己的想法,有了解这方面的童鞋欢迎指正);反之,如2个CPU,回收线程对CPU的消耗会达到50%,这样就严重影响了用户线程的正常响应。

2.无法处理浮动垃圾:无法处理在回收时产生的新垃圾,因此会预留一部分空间存放新垃圾(对象),默认老年代空间被占用达到一定百分比时,会触发FullGC。

默认百分比计算公式:CMSInitiatingOccupancyFraction=(100 - MinHeapFreeRatio) + (CMSTriggerRatio * MinHeapFreeRatio / 100)

MinHeapFreeRatio:空余堆内存小于40%(JDK1.7默认40%)时,JVM就会增大堆直到-Xmx的最大限制,CMSTriggerRatio:JDK1.7默认为80%,因此jdk1.7的CMSInitiatingOccupancyFraction的默认值为92%,也可适当调整该参数以获取较好的性能,通常和-XX:+UseCMSInitiatingOccupancyOnly配合使用,如果没有开启该参数,JVM只是用CMSInitiatingOccupancyFraction所设定的值进行第一次CMS GC,后面都是使用JVM动态计算出来的值来进行GC。当然该参数并不是越大越好,参数过大,导致预留的空间变小,在执行CMS GC的过程中同时有对象要放入老年代,但是老年代没有足够的连续空间时,会抛出Concurrent Mode Failure的错误,此时不得不启用备用收集器Serial Old进行GC,这样停顿的时间就会变得很长,因此,CMSInitiatingOccupancyFraction的值通常建议设置为存活对象(即MinorGC后被转移到老年代的对象)大小的1.5倍,当然,这也只是一个估值,不可能很准确。

3.产生内存碎片

CMS是基于“标记-清除”算法的收集器,这意味着垃圾回收完成后会产生大量的内存碎片,当大对象没有足够的连续空间来分配时,不得不提前触发一次Full GC。

可打开UseCMSCompactAtFullCollection参数,每次Full GC后会进行一次碎片整理,碎片整理时是无法并发的,因此会拖长停顿时间;也可设置CMSFullGCsBeforeCompaction参数值,用于执行多少次不带碎片整理的Full GC后来一次碎片整理。


下面简单测试下CMS收集器各参数的使用效果:

另:参数调整、测试结果及结论我都写在了注释里,方便对比效果。


import java.util.Random;/** * 测试CMS收集器 基于标记-清除算法 以最短GC停顿时间为目标的收集器 * ====================Test CMS && 新生代:老年代=1:1 VS 新生代:老年代=1:2==================== *  * ====================新生代:老年代=1:1==================== * -XX:+UseConcMarkSweepGC -Xms1024m -Xmx1024m -Xmn512m -XX:+PrintGCDetails * 第一次: * runtime:272715.482610ms * 新生代GCtime:3643次,耗时79198ms,平均GC耗时21.7398ms * 老年代GCtime:3302次,耗时54467ms,平均GC耗时16.4952ms * 总GCtime:6945次,耗时133665ms,吞吐量50.99%,concurrent mode failure次数749次,promotion failed次数216次 *  * 第二次: * runtime:249985.544583ms * 新生代GCtime:3601次,耗时71791ms,平均GC耗时19.9364ms * 老年代GCtime:3283次,耗时53024ms,平均GC耗时15.1794ms * 总GCtime:6884次,耗时121625ms,吞吐量51.35%,concurrent mode failure次数737次,promotion failed次数223次 *  * 第三次: * runtime:246530.032659ms * 新生代GCtime:3634次,耗时72208ms,平均GC耗时19.8701ms * 老年代GCtime:3310次,耗时48827ms,平均GC耗时14.7514ms * 总GCtime:6944次,耗时121035ms,吞吐量50.9%,concurrent mode failure次数710次,promotion failed次数264次 *  * ====================新生代:老年代=1:2==================== * -XX:+UseConcMarkSweepGC -Xms1024m -Xmx1024m -XX:+PrintGCDetails * 第一次: * runtime:359245.357208ms * 新生代GCtime:6271次,耗时153111ms,平均GC耗时24.4157ms * 老年代GCtime:3925次,耗时68898ms,平均GC耗时17.5536ms * 总GCtime:10196次,耗时222009ms,吞吐量38.2%,concurrent mode failure次数1153次,promotion failed次数180次 *  * 第二次: * runtime:350564.980612ms * 新生代GCtime:6253次,耗时150799ms,平均GC耗时24.1163ms * 老年代GCtime:3921次,耗时66580ms,平均GC耗时16.9804ms * 总GCtime:10174次,耗时217379ms,吞吐量37.99%,concurrent mode failure次数1141次,failed次数190次 *  * 第三次: * runtime:352798.367248ms * 新生代GCtime:6269次,耗时150110ms,平均GC耗时23.9448ms * 老年代GCtime:3921次,耗时68850ms,平均GC耗时17.5592ms * 总GCtime:10190次,耗时218960ms,吞吐量37.94%,concurrent mode failure次数1162次,promotion failed次数182次 *  * 测试结论:新生代和老年代的配比调整后(1:2),老年代空间比新生代大,但是新生代空间缩小引起的minor GC频率高了近一倍,进而导致老年代的GC频率也变高,尽管老年代空间变大,它的GC次数 * 也还是会增多,cmf错误增多,pf错误有轻微改善,吞吐量大幅下降 *  * ====================Test CMS && -XX:-UseCMSInitiatingOccupancyOnly VS -XX:+UseCMSInitiatingOccupancyOnly==================== * ====================-XX:-UseCMSInitiatingOccupancyOnly==================== * -XX:+UseConcMarkSweepGC -Xms1024m -Xmx1024m -XX:+PrintGCDetails -XX:CMSInitiatingOccupancyFraction=60 -XX:-UseCMSInitiatingOccupancyOnly * 第一次: * runtime:358914.776261ms * 新生代GCtime:6272次,耗时156202ms,平均GC耗时24.9047ms * 老年代GCtime:3887次,耗时68298ms,平均GC耗时17.5788ms * 总GCtime:10159次,耗时224500ms,吞吐量37.45%,concurrent mode failure次数1164次,promotion failed次数176次 *  * 第二次: * runtime:342977.466012ms * 新生代GCtime:6235次,耗时147295ms,平均GC耗时23.6239ms * 老年代GCtime:3887次,耗时66026ms,平均GC耗时16.9863ms * 总GCtime:10122次,耗时213321ms,吞吐量37.8%,concurrent mode failure次数1133次,promotion failed次数178次 *  * 第三次: * runtime:353636.902574ms * 新生代GCtime:6339次,耗时149985ms,平均GC耗时23.6607ms * 老年代GCtime:3969次,耗时69797ms,平均GC耗时17.5855ms * 总GCtime:10308次,耗时219782ms,吞吐量37.85%,concurrent mode failure次数1188次,promotion failed次数194次 *  * ====================-XX:+UseCMSInitiatingOccupancyOnly==================== * -XX:+UseConcMarkSweepGC -Xms1024m -Xmx1024m -XX:+PrintGCDetails -XX:CMSInitiatingOccupancyFraction=60 -XX:+UseCMSInitiatingOccupancyOnly * 第一次: * runtime:341280.937406ms * 新生代GCtime:6344次,耗时147641ms,平均GC耗时23.2725ms * 老年代GCtime:3513次,耗时67306ms,平均GC耗时19.1591ms * 总GCtime:9857次,耗时214947ms,吞吐量37.02%,concurrent mode failure次数1287次,promotion failed次数147次 *  * 第二次: * runtime:340576.789017ms * 新生代GCtime:6288次,耗时146703ms,平均GC耗时23.3306ms * 老年代GCtime:3449次,耗时66673ms,平均GC耗时19.3311ms * 总GCtime:9737次,耗时213376ms,吞吐量37.35%,concurrent mode failure次数1228次,promotion failed次数179次 *  * 第三次: * runtime:338139.391526ms * 新生代GCtime:6298次,耗时147962ms,平均GC耗时23.4935ms * 老年代GCtime:3457次,耗时64833ms,平均GC耗时18.7541ms * 总GCtime:9755次,耗时212794ms,吞吐量37.07%,concurrent mode failure次数1222次,promotion failed次数175次 *  * 测试结论: * 1.配置CMSInitiatingOccupancyFration,未启用UseCMSInitiationOccupancyOnly,观察日志可发现(GC [1 CMS-initial-mark: 430084K(707840K)]),430084/707840=60.76% * 老年代空间占用60%后,触发了第一次CMS GC,CMS开始做初始化标记,第二次CMS GC被触发时,老年代空间占用率([GC [1 CMS-initial-mark: 332806K(707840K)]),332806/707840=47%, * 第三次CMS GC被触发时,老年代空间占用率([GC [1 CMS-initial-mark: 364030K(707840K)]),364030/707840=51%,由此可见,未启用CMSInitiatingOccupancyOnly参数时, * CMSInitiatingOccupancyFraction参数只用作第一次CMSGC,后面都是JVM动态计算后进行的CMS GC。 * 2.启用UseCMSInitiatingOccupancyOnly后,观察日志可发现,每次CMS GC开始初始化标记,老年代空间占用率均>=60%,老年代的GC次数明显减少,但是cmf错误也随之增多,老年代单次GC时间 * 变长 *  * ====================Test CMS && -XX:+UseCMSCompactAtFullCollection==================== * -XX:+UseConcMarkSweepGC -Xms1024m -Xmx1024m -XX:+PrintGCDetails -XX:CMSInitiatingOccupancyFraction=60 -XX:+UseCMSCompactAtFullCollection * 第一次: * runtime:328494.673131ms * 新生代GCtime:6240次,耗时140340ms,平均GC耗时22.4904ms * 老年代GCtime:3932次,耗时63540ms,平均GC耗时16.1597ms * 总GCtime:10172次,耗时203880ms,吞吐量37.94%,concurrent mode failure次数1165次,promotion failed次数191次 *  * 第二次: * runtime:331893.002815ms * 新生代GCtime:6257次,耗时142251ms,平均GC耗时22.7347ms * 老年代GCtime:3929次,耗时63729ms,平均GC耗时16.2202ms * 总GCtime:10186次,耗时205979ms,吞吐量37.94%,concurrent mode failure次数1141次,promotion failed次数187次 *  * 第三次: * runtime:331561.623350ms * 新生代GCtime:6204次,耗时142546ms,平均GC耗时22.9764ms * 老年代GCtime:3870次,耗时63457ms,平均GC耗时16.3971ms * 总GCtime:10074次,耗时206003ms,吞吐量37.87%,concurrent mode failure次数1120次,promotion failed次数191次 *  * 测试结论:理论上开启UseCMSCompactAtFullCollection参数后,要消耗时间去进行内存碎片整理,GC的性能会有所下降,体现在单次GC时间变成,吞吐量下降,碎片少了,老年代的GC次数以及cmf错误理论上也会减少, * 但是此处测试并未证实该结论,可能是测试代码中此处的cmf错误并不是内存碎片引起的。 *  *  * @author ljl */public class TestCMSGC {private static int _5MB = 5 * 1024 * 1024;public static void main(String[] args) {byte[] memory = null;Random rm = new Random();int j;long startTime = System.nanoTime();for (int i = 0; i < 10000; i++) {j = rm.nextInt(10) + 10;System.out.println("j * _5MB = " + j * 5);memory = new byte[j * _5MB];}System.out.println("runtime = " + (System.nanoTime() - startTime));}}


原创粉丝点击