JAVA虚拟机之三:CMS垃圾收集器

时间:2022-12-27 20:59:11

一、CMS垃圾收集器介绍

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间。

JAVA虚拟机之三:CMS垃圾收集器

CMS是一款优秀的收集器,它的最主要优点在名字上已经体现出来了:并发收集、低停顿,Sun的一些官方文档里面也称之为并发低停顿收集器(Concurrent Low Pause Collector)。但是CMS还远达不到完美的程度,它有以下三个显著的缺点:
  • CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程最多占用不超过25%的CPU资源。但是当CPU不足4个时(譬如2个),那么CMS对用户程序的影响就可能变得很大,如果CPU负载本来就比较大的时候,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,这也很让人受不了。为了解决这种情况,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep / i-CMS)的CMS收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记和并发清理的时候让GC线程、用户线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,速度下降也就没有那么明显,但是目前版本中,i-CMS已经被声明为“deprecated”,即不再提倡用户使用。
  • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序的运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好留待下一次GC时再将其清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即还需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数以获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
  • 还有最后一个缺点,在本节在开头说过,CMS是一款基于“标记-清除”算法实现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着收集结束时会产生大量空间碎片。空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费附送一个碎片整理过程,内存整理的过程是无法并发的。空间碎片问题没有了,但停顿时间不得不变长了。虚拟机设计者们还提供了另外一个参数-XX: CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。
二、CMS收集器测试
在eclipse中配置参数如下图:
JAVA虚拟机之三:CMS垃圾收集器
完整配置参数:
-server -verbose:gc -Xms512m -Xmx512m -Xmn192m -XX:PermSize=32m -XX:MaxPermSize=32m -Xss256k -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=4 -XX:+UseCMSCompactAtFullCollection -XX:CMSMaxAbortablePrecleanTime=5000 -XX:CMSFullGCsBeforeCompaction=5 -XX:CMSInitiatingOccupancyFraction=85 -XX:+UseParNewGC -Xloggc:D:/logs/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/logs/HeapDumpOnOutOfMemoryError.log -XX:+DisableExplicitGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
代码Test1.java
<span style="font-family:SimHei;font-size:10px;">package com.tools138.com;
/**
*
* @author alaric
*
*/
public class Test2 {

private static int n = 20;
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub

byte[] b1 = getM(50);
byte[] b2 = getM(50);
byte[] b3 = getM(50);
byte[] b4 = getM(50);
byte[] b5 = getM(50);
byte[] b6 = getM(50);
byte[] b7 = getM(5);
byte[] b8 = getM(5);
byte[] b9 = getM(5);
byte[] b10 = getM(5);
byte[] b11 = getM(5);
byte[] b12 = getM(5);
byte[] b13 = getM(5);
byte[] b14 = getM(5);
byte[] b15 = getM(5);
byte[] b16 = getM(5);
byte[] b17 = getM(5);
byte[] b18 = getM(5);
byte[] b19 = getM(5);
byte[] b20 = getM(100);
byte[] b21 = getM(100);
byte[] b22 = getM(100);
byte[] b23 = getM(100);

// Thread.sleep(2000);

}

public static byte[] getM(int m) {
return new byte[1024 * 1024 * m];
}

}</span>

因为用的是JDK1.8测试来测试的,所以PermSize,MaxPermSize已经在java8中移除,UseCMSCompactAtFullCollection,CMSFullGCsBeforeCompaction已经过时。 控制台输出如下:
JAVA虚拟机之三:CMS垃圾收集器

上面的java测试代码可以随意进行添加修改,来测试CMS收集器的收集的各个阶段。
gc.log输出:
<span style="font-family:SimHei;font-size:10px;">Java HotSpot(TM) 64-Bit Server VM (25.65-b01) for windows-amd64 JRE (1.8.0_65-b17), built on Oct  6 2015 16:39:20 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 4094316k(1410832k free), swap 8186796k(3113544k free)
CommandLine flags: -XX:CMSFullGCsBeforeCompaction=5 -XX:CMSInitiatingOccupancyFraction=85 -XX:CMSMaxAbortablePrecleanTime=5000 -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:/logs/HeapDumpOnOutOfMemoryError.log -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=1048144896 -XX:MaxNewSize=201326592 -XX:MaxTenuringThreshold=6 -XX:NewSize=201326592 -XX:OldPLABSize=16 -XX:ParallelGCThreads=4 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:ThreadStackSize=256 -XX:+UseCMSCompactAtFullCollection -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC
0.279: [GC (Allocation Failure) 0.279: [ParNew: 114985K->553K(176960K), 0.0395416 secs] 114985K->102956K(504640K), 0.0398840 secs] [Times: user=0.17 sys=0.00, real=0.04 secs]
0.353: [GC (Allocation Failure) 0.353: [ParNew: 157128K->715K(176960K), 0.1367165 secs] 259530K->256719K(504640K), 0.1368421 secs] [Times: user=0.47 sys=0.01, real=0.14 secs]
0.498: [GC (CMS Initial Mark) [1 CMS-initial-mark: 256004K(327680K)] 307919K(504640K), 0.0002677 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.498: [CMS-concurrent-mark-start]
0.506: [CMS-concurrent-mark: 0.008/0.008 secs] [Times: user=0.02 sys=0.02, real=0.01 secs]
0.506: [CMS-concurrent-preclean-start]
0.507: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.507: [CMS-concurrent-abortable-preclean-start]
0.507: [GC (Allocation Failure) 0.507: [ParNew: 121133K->16165K(176960K), 0.0562811 secs] 377137K->374569K(540508K), 0.0563903 secs] [Times: user=0.19 sys=0.01, real=0.06 secs]
0.577: [GC (Allocation Failure) 0.577: [ParNew: 118565K->0K(176960K), 0.0739984 secs] 476969K->476703K(658284K), 0.0741047 secs] [Times: user=0.22 sys=0.02, real=0.07 secs]
0.652: [CMS-concurrent-abortable-preclean: 0.001/0.145 secs] [Times: user=0.41 sys=0.03, real=0.14 secs]
0.667: [GC (CMS Final Remark) [YG occupancy: 102400 K (176960 K)]0.667: [Rescan (parallel) , 0.0247611 secs]0.692: [weak refs processing, 0.0000398 secs]0.692: [class unloading, 0.0002673 secs]0.692: [scrub symbol table, 0.0004401 secs]0.693: [scrub string table, 0.0001223 secs][1 CMS-remark: 476703K(481324K)] 579103K(658284K), 0.0258248 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
0.693: [CMS-concurrent-sweep-start]
0.693: [GC (Allocation Failure) 0.693: [ParNew: 102400K->0K(176960K), 0.1390355 secs] 579103K->579103K(760688K), 0.1391037 secs] [Times: user=0.31 sys=0.03, real=0.14 secs]
0.832: [CMS-concurrent-sweep: 0.139/0.139 secs] [Times: user=0.31 sys=0.03, real=0.14 secs]
0.833: [CMS-concurrent-reset-start]
0.836: [CMS-concurrent-reset: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.851: [GC (Allocation Failure) 0.851: [ParNew: 102400K->0K(176960K), 0.1025667 secs] 681504K->681504K(1004352K), 0.1026574 secs] [Times: user=0.14 sys=0.08, real=0.10 secs]
0.968: [GC (CMS Initial Mark) [1 CMS-initial-mark: 681504K(827392K)] 783904K(1004352K), 0.0001363 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.968: [CMS-concurrent-mark-start]
Heap
par new generation total 176960K, used 103973K [0x00000000c1800000, 0x00000000cd800000, 0x00000000cd800000)
eden space 157312K, 66% used [0x00000000c1800000, 0x00000000c7d89600, 0x00000000cb1a0000)
from space 19648K, 0% used [0x00000000cb1a0000, 0x00000000cb1a0000, 0x00000000cc4d0000)
to space 19648K, 0% used [0x00000000cc4d0000, 0x00000000cc4d0000, 0x00000000cd800000)
concurrent mark-sweep generation total 827392K, used 681504K [0x00000000cd800000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2612K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 287K, capacity 386K, committed 512K, reserved 1048576K</span>
主要关注一下红色框起来的部分。

JAVA虚拟机之三:CMS垃圾收集器

对上图中一条完整收集记录进行解释:

JAVA虚拟机之三:CMS垃圾收集器

[ParNew:  表示年轻代GC,用ParNew 收集器收集;
157128K->715K(176960K) :157128K 表示年轻代收集前的大小,715K表示收集后的大小,(176960K)表示当前总容量大小;
, 0.1367165 secs] :表示收集年轻代所花费的时间,单位秒;
 259530K->256719K(504640K), 0.1368421 secs]:表示当前对的收集前大小,收集后大小和容量总大小及收集花费的时间。
[Times: user=0.47 sys=0.01, real=0.14 secs]: 表示用户耗时,系统耗时,真实耗时
 
再回到上面大图,看红色框起来的数据:
0.279秒和0.353秒时候两次ParNew在新生代分配失败(Allocation Failure),对象直接进入老年代;
0.498秒时老年代CMS初始标记(Initial-mark);
0.498秒进行并发标记(concurrent-mark-start);
0.506秒开始进行预清理(concurrent-preclean-start);
0.667秒(CMS Final Remark);
0.693秒开始并发清理开始(concurrent-sweep);
0.833秒线程参数重置(reset);
 
三、CMS垃圾收集器总结
当使用CMS收集器时,当开始进行收集时,old代的收集过程如下所示:
1、首先jvm根据-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly来决定什么时间开始垃圾收集;
2、如果设置了-XX:+UseCMSInitiatingOccupancyOnly,那么只有当old代占用确实达到了-XX:CMSInitiatingOccupancyFraction参数所设定的比例时才会触发cms gc;
3、如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,那么系统会根据统计数据自行决定什么时候触发cms gc;因此有时会遇到设置了80%比例才cms gc,但是50%时就已经触发了,就是因为这个参数没有设置的原因;
4、当cms gc开始时,首先的阶段是CMS-initial-mark,此阶段是初始标记阶段,是stop the world阶段,因此此阶段标记的对象只是从root集最直接可达的对象;
     [1 CMS-initial-mark: 256004K(327680K)] 307919K(504640K),,指标记时,old代的已用空间和总空间
5、下一个阶段是CMS-concurrent-mark,此阶段是和应用线程并发执行的,所谓并发收集器指的就是这个,主要作用是标记可达的对象
       此阶段会打印2条日志:CMS-concurrent-mark-start,CMS-concurrent-mark
6、下一个阶段是CMS-concurrent-preclean,此阶段主要是进行一些预清理,因为标记和应用线程是并发执行的,因此会有些对象的状态在标记后会改变,此阶段正是解决这个问题因为之后的Rescan阶段也会stop the world,为了使暂停的时间尽可能的小,也需要preclean阶段先做一部分工作以节省时间
     此阶段会打印2条日志:CMS-concurrent-preclean-start,CMS-concurrent-preclean
7、下一阶段是CMS-concurrent-abortable-preclean阶段,加入此阶段的目的是使cms gc更加可控一些,作用也是执行一些预清理,以减少Rescan阶段造成应用暂停的时间
     此阶段涉及几个参数:
     -XX:CMSMaxAbortablePrecleanTime:当abortable-preclean阶段执行达到这个时间时才会结束
     -XX:CMSScheduleRemarkEdenSizeThreshold(默认2m):控制abortable-preclean阶段什么时候开始执行,
      即当eden使用达到此值时,才会开始abortable-preclean阶段
     -XX:CMSScheduleRemarkEdenPenetratio(默认50%):控制abortable-preclean阶段什么时候结束执行
      此阶段会打印一些日志如下:
     CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,
      CMS:abort preclean due to time XXX
8、再下一个阶段是第二个stop the world阶段了,即Rescan阶段,此阶段暂停应用线程,对对象进行重新扫描并标记;
      [YG occupancy: 102400 K (176960 K)],指执行时young代的情况
      [1 CMS-remark: 476703K(481324K)] ,指执行时old代的情况
      此外,还打印出了弱引用处理、类卸载等过程的耗时
9、再下一个阶段是CMS-concurrent-sweep,进行并发的垃圾清理
10、最后是CMS-concurrent-reset,为下一次cms gc重置相关数据结构
11、full gc:
有2种情况会触发full gc,在full gc时,整个应用会暂停
       A,concurrent-mode-failure:当cms gc正进行时,此时有新的对象要进行old代,但是old代空间不足造成的
       B,promotion-failed:当进行young gc时,有部分young代对象仍然可用,但是S1或S2放不下,因此需要放到old代,但此时old代空间无法容纳此。
影响cms gc时长及触发的参数是以下2个: 
        -XX:CMSMaxAbortablePrecleanTime=5000
        -XX:CMSInitiatingOccupancyFraction=80
解决也是针对这两个参数来的,根本的原因是每次请求消耗的内存量过大
解决方式:
      A,针对cms gc的触发阶段,调整-XX:CMSInitiatingOccupancyFraction=50,提早触发cms gc,就可以缓解当old代达到80%,cms gc处理不完,从而造成concurrent mode failure引发full gc
     B,修改-XX:CMSMaxAbortablePrecleanTime=500,缩小CMS-concurrent-abortable-preclean阶段的时间
     C,考虑到cms gc时不会进行compact,因此加入-XX:+UseCMSCompactAtFullCollection
       (cms gc后会进行内存的compact)和-XX:CMSFullGCsBeforeCompaction=4(在full gc4次后会进行compact)参数
 
 
参考资料:
1、深入理解java虚拟机-周志民