虽然新出的G1优点明显,但是CMS算法依然是目前项目中使用的最多的垃圾收集器,G1可能还有待时间的验证,目前只能是实验阶段,还未大规模普及。
1、CMS收集器
Concurrent Mark Sweep (CMS) Collector
The Concurrent Mark Sweep (CMS) collector is designed for applications that prefer shorter garbage collection pauses and
that can afford to share processor resources with the garbage collector while the application is running.
Typically applications that have a relatively large set of long-lived data (a large old generation) and
run on machines with two or more processors tend to benefit from the use of this collector.
The CMS collector is enabled with the command-line option -XX:+UseConcMarkSweepGC.
The CMS collector is deprecated. Strongly consider using the Garbage-First collector instead.
从官网的描述可以看出
,CMS是一款更注重短暂停时间的高性能垃圾收集器,JDK7/8默认的垃圾收集器。
虽然现在项目中都是使用的CMS垃圾收集器,但因为有了G1收集器,最终还是建议放弃“deprecated”CMS使用G1垃圾收集器,之所以还有这么多的人使用CMS,可能还是希望G1的性能保证能被岁月更多的考验和优化,能让其变的更加优秀。要想更好的理解G1为什么优秀,还是要先理解历史的垃圾收集器的升级变迁过程,所以有必要理解一下CMS垃圾收集。
HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,
第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。目前很大一部分的Java应用集中在B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,而这也恰恰是CMS所擅长的地方。
CMS(Concurrent Mark Sweep)收集器的设计目标是:
获取最短回收停顿时间的收集器。
2、CMS收集过程
CMS的垃圾收集一共需要经过四个阶段:
-
1. 初始标记:此阶段会产生STW;这个过程只是标记出GC ROOTS能直接关联到的对象,耗时短,速度很快;
-
2. 并发标记: 这个步是根据前面提到的GC ROOTS 根搜索算法,会判定对象是“存活”还是“已死”;比如说 A -> B (A 引用 B,假设 A 是 GC Roots 关联到的对象),那么这个阶段就是标记出 B 对象, A 对象会在初始标记中标记出来。
-
3. 重新标记: 此阶段会产生STW,主要是为了检查校验并发标记期间,因用户程序继续运行而产生变动的那一部分对象的标记记录和新对象;
-
4. 并发清除:该步就是清除系统中无用“以死”的对象;之后将为下次gc做准备;
由于整个过程中并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。

3、CMS垃圾收集实践
CMS垃圾收集参数设置:
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC -Xloggc:CMS-GC.log
通过日志就可以反映出垃圾收集器的工作过程和原理:
Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for bsd-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:35:23 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 16777216k(404032k free)
/proc/meminfo:
CommandLine flags: -XX:CMSInitiatingOccupancyFraction=30 -XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:MaxNewSize=1400832 -XX:MaxTenuringThreshold=6 -XX:NewSize=1400832 -XX:OldPLABSize=16 -XX:OldSize=2793472
-XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
2016-07-07T17:46:27.783-0800: 0.126: [GC (Allocation Failure) 2016-07-07T17:46:27.783-0800: 0.126: [ParNew: 1088K->128K(1216K), 0.0027193 secs] 1088K->435K(6016K), 0.0028139 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-07-07T17:46:28.192-0800: 0.535: [GC (CMS Initial Mark) [1 CMS-initial-mark: 1969K(4800K)] 2110K(6016K), 0.0007840 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-07-07T17:46:28.193-0800: 0.535: [CMS-concurrent-mark-start]
2016-07-07T17:46:28.194-0800: 0.537: [CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-07-07T17:46:28.194-0800: 0.537: [CMS-concurrent-preclean-start]
2016-07-07T17:46:28.194-0800: 0.537: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-07-07T17:46:28.194-0800: 0.537: [GC (CMS Final Remark) [YG occupancy: 140 K (1216 K)]
2016-07-07T17:46:28.194-0800: 0.537: [Rescan (parallel) , 0.0002201 secs]
2016-07-07T17:46:28.194-0800: 0.537: [weak refs processing, 0.0000296 secs]
2016-07-07T17:46:28.194-0800: 0.537: [class unloading, 0.0002271 secs]
2016-07-07T17:46:28.195-0800: 0.537: [scrub symbol table, 0.0004065 secs]
2016-07-07T17:46:28.195-0800: 0.538: [scrub string table, 0.0001614 secs][1 CMS-remark: 1969K(4800K)] 2110K(6016K), 0.0011155 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-07-07T17:46:28.195-0800: 0.538: [CMS-concurrent-sweep-start]
2016-07-07T17:48:08.415-0800: 0.611: [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2016-07-07T17:48:08.415-0800: 0.611: [CMS-concurrent-reset-start]
结果分析:youngGC里的ParNew就不分析了,这里主要看一下CMS的收集过程:
Phase 1: CMS-initial-mark
这是CMS中两次stop-the-world事件中的一次。它有两个目标:一是标记老年代中所有的GC Roots;二是标记被年轻代中活着的对象引用的对象。
Phase 2: CMS-concurrent-mark-start
这个阶段会遍历整个老年代并且标记所有存活的对象,从“初始化标记”阶段找到的GC Roots开始。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
Phase 3: CMS-concurrent-preclean
这个阶段又是一个并发阶段,和应用线程并行运行,不会中断他们。前一个阶段在并行运行的时候,一些对象的引用已经发生了变化,当这些引用发生变化的时候,JVM会标记堆的这个区域为Dirty Card(包含被标记但是改变了的对象,被认为"dirty"),这就是 Card Marking。
Phase 4: CMS Final Remark
这个阶段是CMS中第二个并且是最后一个STW的阶段。该阶段的任务是完成标记整个年老代的所有的存活对象。由于之前的预处理是并发的,它可能跟不上应用程序改变的速度,这个时候,STW是非常需要的来完成这个严酷考验的阶段。
Phase 5: CMS-concurrent-sweep-start
和应用线程同时进行,不需要STW。这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。
Phase 6: CMS-concurrent-reset-start
这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
4、CMS垃圾收集优缺点
目标
|
目标与意义:减少垃圾收集停顿时间,真正实现了垃圾收集和任务线程并行工作;
|
优点
|
并发收集
,此阶段比较耗时;由于采用并发收集可以减少停顿时间;
|
缺点
|
(
1)CMS收集器对CPU资源非常敏感。由于和用户线程一起工作,CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,出现cpu资源的竞争,收集期间可能出现卡顿现象;
(2)
CMS收集器无法处理浮动垃圾,由于和用户线层一起工作,期间无法处理新增的浮游垃圾;
(3)CMS是基于“标记-清除”算法实现的收集器,手机结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC;
(4)
产生2次STW,并发阶段降低吞吐量;
|
使用场景
|
(1) 多核cpu,追求低停顿时间,牺牲吞吐量来获取较小的暂停时间;
(2) 适用于GC不频繁的老年代,青年代使用的是ParaNew收集器;
|
参数使用
|
csm参数解析说明:
-XX:+UseConcMarkSweepGC 此参数将启动 CMS 回收器。默认新生代是 ParNew + CMS,也可以设置 Serial 为新生代收集器。该参数等价于 -Xconcgc。
-XX:ParallelGCThreads 指定线程数。默认并发线程数是:(ParallelGCThreads + 3)/ 4)。
-XX:CMSInitiatingOccupancyFraction 由于 CMS 回收器不是独占式的,在垃圾回收的时候应用程序仍在工作,所以需要留出足够的内存给应用程序,否则会触发 FGC。而什么时候运行 CMS GC 呢?通过该参数即可设置,该参数表示的是老年代的内存使用百分比。当达到这个阈值就会执行 CMS。
默认是68。 如果老年代内存增长很快,建议降低阈值,避免 FGC,如果增长慢,则可以加大阈值,减少 CMS GC 次数。提高吞吐量。
-XX:+
UseCMSCompactAtFullCollection 由于 CMS 使用标记清理算法,内存碎片无法避免。该参数指定每次 CMS 后进行一次碎片整理。
-XX:CMSFullGCsBeforeCompaction 由于每次进行碎片整理将会影响性能,你可以使用该参数设定多少次 CMS 后才进行一次碎片整理,也就是内存压缩。
-XX:CMSInitiatingPermOccupancyFraction 当永久区占用率达到这一百分比时,启动 CMS 回收(前提是 -XX:+CMSClassUnloadingEnabled 激活了)。
-XX:UseCMSInitiatingOccupancyOnly 表示只在到达阈值的时候才进行 CMS 回收。
XX:CMSWaitDuration=2000 由于CMS GC 条件比较简单,JVM有一个线程定时扫描Old区,时间间隔可以通过该参数指定(毫秒单位),默认是2s。
|
项目实战参数设置:
MEMORY_JVM=" -Xmx4g -Xms4g -Xmn1G -Xss512K
-XX:PermSize=128m
-XX:MaxPermSize=256m
-XX:SurvivorRatio=6
-XX:InitialCodeCacheSize=64m
-XX:ReservedCodeCacheSize=64m"
GC_JVM=" -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSFullGCsBeforeCompaction=0
-XX:+ExplicitGCInvokesConcurrent
-XX:+UseCMSCompactAtFullCollection"
4.1、分配担保机制
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,那么Minor GC可以确保是安全的。
新生代使用复制收集算法,为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况是内存回收之后,新生代中所有的对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象存活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败,那就只好在失败后重新发起一次FULL GC。
4.2、CMS的问题
cms的promotion failed & concurrent mode failure问题
问题分析: 尽管CMS使用一个叫做
分配担保的机制,每次Minor GC之后要保证新生代的空间
survivor + eden > 老年带的空闲空间,但是对象分配是不可预测的,总会有写对象分配在老年带是满足不了的。
该问题是在进行MinorGC时,Survivor Space放不下,或者申请的大对象需要直接进入老年代,而此时老年代没有足够的空间来存储这个对象(其中一个原因就是老年带有足够的空闲空间,但是比较
碎片化不连续导致申请失败),这时就会产生
promotion failed问题。很不幸,此时又有业务线程来申请新的内存空间,让本已经不够的堆内存雪上加霜,无法满足空间需求,就会进而出现下一个问题
:
concurrent mode filure,此时只能Stop-The-Wold,垃圾回收降级为
GC-Serial Old,JVM将采用后备的串型收集器来进行垃圾回收,此时的CMS实际已经没有什么意义:
解
决
promotion failed
:碎片整理 + 增大年轻代空间
-
-XX:UseCMSCompactAtFullCollection 每次CMS完进行碎片的整理;
-
-XX:CMSFullGCBeforeCompaction=0 每次 gc 后进行一次整理碎片操作;
-
-Xmn XX:SurvivorRatio 默认为8,通过它调大新生代或者救助空间,避免对象直接进入老年代 ;
解决
concurrent mode failure
:提前触发CMS + 增大老年代空间
-
+XX:CMSInitiatingOccupancyFraction 提前进行CMS垃圾回收,预留足够的老年代内存空间,默认的值是68%, 如果项目确定有大对象的产生, 可以适当的调小,但是相应CMS的GC耗时将增加;
-
-Xms -Xmx 直接调大老年带的空间;
5、小结
CMS收集器是一种以获取最短回收停顿时间为目标的收集器,具体的使用还的根据项目的场景来设置相关的参数,注意碎片问题。
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
参考资料:
《深入了解jvm虚拟机》