cms垃圾收集器是如何进行垃圾回收的

CMS 垃圾回收器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用集中在互联网网站或者 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。从名字(包含"Mark Sweep")上就可以看出,CMS 收集器是基于"标记-清除"算法实现的。

CMS 垃圾收集器属于老年代的收集器,以对应的新生代收集器为parNew。

核心垃圾回收过程

整个过程分为4个步骤,包括:

  1. 初始标记(CMS initial mark):初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
  2. 并发标记(CMS concurrent mark):该阶段就是进行 GC Roots Tracing 的过程;
  3. 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短;
  4. 并发清除(CMS concurrent sweep)

其中,初始标记、重新标记这两个步骤仍然需要 “Stop The World”。由于整个过程中耗时最长的并发标记和并发清除过程收集器收集线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
在这里插入图片描述

完整垃圾回收过程

CMS 完整处理过程有七个步骤:

  1. 初始标记(CMS-initial-mark) ,会导致swt(stop the world);
  2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;
  3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;
  4. 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
  5. 重新标记(CMS-remark) ,会导致swt;
  6. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
  7. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

阶段一:初始标记

这是CMS中两次stop-the-world事件中的一次。这一步的作用是标记存活的对象,有两部分:

  • 标记老年代中所有的GC Roots对象(即直接被 GC Root 引用的对象),如下图节点1;

  • 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻带中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3;

在这里插入图片描述
在Java语言里,可作为GC Roots对象的包括如下几种:

  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;
  • 方法区中的类静态属性引用的对象 ;
  • 方法区中的常量引用的对象 ;
  • 本地方法栈中JNI的引用的对象;
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象、一些常驻的异常对象(比如NullPointException,OutOfMemoryException)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等

ps:为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数;

阶段二:并发标记(Concurrent Mark)

在这个阶段垃圾收集器会遍历老年代,然后标记所有存活的对象,它会根据上个阶段找到的 GC Roots 遍历查找。

并发标记阶段,它会与用户的应用程序并发运行。并不是老年代的所有存活的对象都会被标记,因为在标记期间用户的程序可能会改变一些引用。(例如节点 3 下面节点的引用发生了改变)

从“初始标记”阶段标记的对象开始找出所有存活的对象;

因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。

关于JVM卡标记(Card Marking),卡表(Card Table),写屏障(Write Barrier),参考:JVM之卡表(Card Table)

为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;

如下图所示,也就是节点1、2、3,最终找到了节点4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
在这里插入图片描述

阶段三:并发预清理阶段

这也是一个并发阶段,与应用的线程并发运行,并不会 Stop 应用的线程,在并发运行的过程中,一些对象的引用可能会发生改变,但是这种情况发生时, JVM 会将包含这个对象的区域(Card)标记为 Dirty,这就是 Card marking。

在 Pre-clean 阶段,那些能够从 Dirty 对象到达的对象也会被标记,这个标记做完之后, Dirty Card 标记就会被清除了。

前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Dirty的Card。如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;
在这里插入图片描述
最后将6标记为存活,如下图所示:

在这里插入图片描述

阶段四:可终止的预处理

这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生abort的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻代的引用,使得下个阶段的重新标记阶段,扫描年轻代指向老年代的引用的时间减少;

阶段五:重新标记

这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。

为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”。

当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻带的对象无用的对象,并将对象放入幸存代或晋升到老年代,这样再进行年轻代扫描时,只需要扫描幸存代的对象即可,一般幸存代非常小,这大大减少了扫描时间。

由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻代的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候,是为了减少连续 STW 发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。

另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled

至此,老年代所有存活的对象都被标记过了,现在可以通过清除算法去清理老年代不再使用的对象

阶段六:并发清理

通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。
这个阶段主要是清除那些没有标记的对象并且回收空间;

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

在这里插入图片描述

阶段七:并发重置

这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。

CMS 优缺点总结

优点:并发收集、低停顿

通过将大量工作分散到并发处理阶段来减少 STW 时间

CMS收集器有3个明显的缺点:

缺点1:总吞吐量降低

CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。

默认启动的回收线程数为:(CPU 数量 + 3)/ 4,CPU 数量=4,回收线程占用 25 %左右的 CPU 资源,CPU 数量越多占用率越低。数量很小使用可以采用增量式并发收集器 i-CMS(Incremental Concurrent Mark Sweep),即在并发标记和清理的时候让 GC 线程和用户线程交替运行,减少独占,但是收集时间变长了,不建议使用。

缺点2:无法处理浮动垃圾

CMS收集器无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为"浮动垃圾"。

因为垃圾收集阶段用户线程也在运行,就会不断产生垃圾,所以得预留一部分空间给用户线程使用,则不能等到老年代几乎全部被填满之后才进行垃圾回收, 1.6 之后当老年代使用 92%,CMS 就启动了,该值可以通过参数:-XX:CMSInitiatingOccupancyFraction 指定,该百分值太小则 GC 过于频繁,太大会导致预留内存无法满足程序需要,出现 “Concurrent Mode Failure”,这时候只能采用 Serial Old 收集器来进行老年代垃圾收集,更加浪费时间。

缺点3:空间碎片问题

CMS是一款基于标记-清除算法实现的收集器,所以会有空间碎片的现象,当空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

解决方案:通过参数:-XX:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 CMS 收集器顶不住要进行 Full GC 时候开启内存碎片合并整理过程,该过程无法并发,停顿时间较长。

补充参数:-XX+CMSFullGCsBeforeCompaction 用于设置执行多少次不压缩的 Full GC 之后,跟着来一次带压缩的。默认值为 0 ,表示每次进入 Full GC 都进行碎片整理。

缺点4:GC 的时间难以预估

由于缺点2和缺点3的存在,对于堆比较大的应用, GC 的时间难以预估

代码验证及CMS GC日志解析

设置虚拟机参数为:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC 因为 CMS 只能运行到老年代,对应的新生代会自动采用与 CMS 对应的垃圾回收器

新生代为9216K

eden区为8/10,为8192K,

survivor from,to各自为1/10,为1024K

老年代为10240K,堆大小一共是10240+10240K = 20M

程序为:

package com.gjxaiou.gc;

/**
 * @Author GJXAIOU
 * @Date 2019/12/18 13:19
 */
public class MyTest5 {
    public static void main(String[] args) {
        int size = 1024 * 1024;
        byte[] myAlloc1 = new byte[4 * size];
        System.out.println("----111111111----");
        byte[] myAlloc2 = new byte[4 * size];
        System.out.println("----222222222----");
        byte[] myAlloc3 = new byte[4 * size];
        System.out.println("----333333333----");
        byte[] myAlloc4 = new byte[2 * size];
        System.out.println("----444444444----");
    }
}

输出结果:

// 前面没有执行任何的垃圾回收,因为 Eden 区域放置 4M 对象可以放下
----111111111----
// 因为第二次 new 又需要分配 4M 空间,Eden 空间不够用,使用垃圾回收,对应新生代是 ParNew 收集器
[GC (Allocation Failure) [ParNew: 5899K->670K(9216K), 0.0016290 secs] 5899K->4768K(19456K), 0.0016630 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
----222222222----

// 新生代垃圾回收
[GC (Allocation Failure) [ParNew: 5007K->342K(9216K), 0.0023932 secs] 9105K->9168K(19456K), 0.0024093 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

// 老年代垃圾回收    
// cms-initial-mark 
// 8825K(10240K)  --  老年代存活对象占用空间大小(老年代总的空间大小)
// 13319K(19456K) --  整个堆存活对象占用空间大小(堆的空间大小)
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8825K(10240K)] 13319K(19456K), 0.0003398 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]
----333333333----
----444444444----

Heap
 par new generation   total 9216K, used 6780K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  78% used [0x00000000fec00000, 0x00000000ff2499d0, 0x00000000ff400000)
  from space 1024K,  33% used [0x00000000ff400000, 0x00000000ff455a08, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 concurrent mark-sweep generation total 10240K, used 8825K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3144K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 343K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

另一段代码,将上面代码中的 byte[] myAlloc4 = new byte[2 * size]; 修改为:byte[] myAlloc4 = new byte[3 * size];得到的结果如下:

----111111111----
[GC (Allocation Failure) [ParNew: 5765K->637K(9216K), 0.0024098 secs] 5765K->4735K(19456K), 0.0024726 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
----222222222----
[GC (Allocation Failure) [ParNew: 4974K->240K(9216K), 0.0041475 secs] 9072K->9060K(19456K), 0.0041812 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
----333333333----
----444444444----
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8819K(10240K)] 16522K(19456K), 0.0002890 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[CMS-concurrent-mark-start]

Heap
 par new generation   total 9216K, used 7764K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  91% used[CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
// 并发预清理 preclean
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
// 可终止的预处理 abortable-preclean
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 [0x00000000fec00000, 0x00000000ff358e70, 0x00000000ff400000)
  from space 1024K,  23% used [0x00000000ff400000, 0x00000000ff43c2d0, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 concurrent mark-sweep generation total 10240K, used 8819K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3126K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 338K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

CMS应用

在中间件中,当需要尽量小的gc停顿时间时,会使用CMS

  • 查看logstash和elasticsearch进程,其young generation使用ParNew,old generation使用CMS
  • rocketmq,其NameSrv使用的是ParNew,CMS
  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值