概述

CMS垃圾收集器是一款优秀的老年代并发垃圾收集器,通过与用户线程并发执行的方式减少GC停顿的时间。本文主要聊一下CMS设计到的相关的数据结构、具体的执行过程、运行中会出现的异常情况。

在CMS之前并行垃圾收集器通过下图方式进行,虽然GC阶段多线程并行执行单此时用户线程是完全暂停的。如果GC时间过长,将引发服务响应超时、调用接口超时等各类异常。

而CMS垃圾收集器大部分时间GC线程与用户线程并发执行,只有在初始标记和重新标记阶段才暂停用户线程

总体思路:当达到GC条件时,开始并发标记存活的对象,并发的过程中记录对象引用关系的变化。并发标记结束后,暂停用户线程,处理引用关系变化,得到所有存活的对象和可以清理的对象。最终并发清理掉不被使用的对象(存在已经不被引用但本次清理不掉的对象)。

初始标记

这是一个STW过程,主要分两步
1、标记GC Roots可达的老年代对象;
2、遍历GC Roots下的新生代对象能够可达的老年代对象;
3、此过程不对以上可达的老年代对象进行进一步的可达扫描。

暂停应用程序线程,遍历GC ROOTS直接可达的对象并将其压入标记栈(mark-stack)。标记完之后恢复应用程序线程。

并发标记

这个阶段虚拟机会分出若干线程(GC 线程)去进行并发标记。标记哪些对象呢,标记那些GC ROOTS最终可达的对象。具体做法是推出标记栈里面的对象,然后递归标记其直接引用的子对象(如果遇到地址比当前对象低的对象则标记并压如栈中,遇到地址比当前对象高则只标记不入栈,同样的把子对象压到标记栈中,重复推出,压入。。。直至清空标记栈。这个阶段GC线程和应用程序线程同时运行。

标记过程举例
标记的过程伴随着对象引用的修改,下图举例说明了并发标记的过程

初始标记(图中未体现,参考步骤a):初始标记时对a进行了标记。
步骤a:对a引用的对象bc,及b引用的对象e进行标记,根据当时的对象引用关系,abcegd是存活的。
步骤b:对象引用关系发生了改变,b不再引用c新增引用d,g不再引用d
步骤c:完成了并发标记的过程,abceg被标记。第二个和第四个区域内的对象引用关系发生了改变,被记录了下来。这里面运用到了card table、mod union table数据结构和write barrier技术,后面进行说明。
步骤d:实际上是重新标记后的结果,可以看到对象d在并发标记结束时未进行标记,但是它还在被对象b引用,不应该回收。这就依赖重新标记阶段对dirty card(对象引用关系发生变化的区域)的处理。

这个过程应用线程在运行,可能Young GC也会发生,会发生以下的情况:
1、新生代对象晋升到老年代
2、在老年代分配对象
3、新老年代对象的引用发生变化

三色标记详解:三色标记法与读写屏障 - 简书

这种条件下可能会出现活动对象的漏标的情况,比如下面场景:

活动对象被遗漏标记

A是活动对象,A->B,标记B可达,将其压入标记栈,此时A所有直接子对象遍历完,A出栈,标记线程将不会再访问A。

同时应用程序移掉了B对C的引用,让A重新引用C。

B出栈时无法标记C可达,A虽然引用C但标记线程不会再访问A,此时C会被当成不可达对象

并发过程中变化的维护card table与mod union table

card table

CMS中一个与YGC相关并十分重要的数据结构是:卡表(card table)。之所以出现卡表这样的一个数据结构是因为:YGC时为了标记活动标记对象除了tracing GC ROOTS之外, 别忘了老年代里也可能会引用新生代对象。所以正常来说还要扫描一次老年代,如果是扫描整个老年代这将会随着堆的增大变得越来越慢,特别是现在内存都越来越大了。所以为了提升性能就引入卡表。

卡表提升性能的原理:逻辑上把老年代内存分成一个个大小相等的卡页(card page,大小512byte),然后对每个卡片准备一个与其对应的标记位,并将这些位集中起管理就好像一个表格(mark table)一样,当改写对象引用是从老年代指向新生代时,在老年代对应的卡片标记位上设置标志位即可,通常这样的卡片我们称之为dirty card。这项操作可以通过上面的提到的write barrier来实现,这样就算对象跨多张卡片也不会有什么问题。卡表通常是用byte数组实现的,byte的值只能取[0,1]这两种。所以btye[i] = 1 就表示第i + 1 卡片所在内存上有指向新生代引用的老年代对象,这时只要tracing这个卡片上的对象即可。如果每个card大小的是128字节(1024位,)那卡表就只占整个老年代的1/1024之一。所以遍历卡表的时间会远比遍历整个老年代快得多!这其中背后思想就是典型以空间换时间的思路!这种思路在G1中也有体现,只不其对应的数据是remember set而已。

对于新生代:它记录老年代到新生代的引用,younggc时不用遍历整个老年代

对于老年代:它记录并发标记开始引用发生变化的card,并发标记结束后需要处理这些card

由于新生代GC与老年代GC同时使用card table,所以会出现冲突的情况

Young GC如果发生,比方说:
1、并发标记还未扫描到脏卡1.
2、Young GC扫描完脏卡,并改变dirty到clean.
3、并发标记扫描,发现卡1已不是脏卡,则不会处理,这就造成了漏标。

但这个card必须在remark阶段进行重新标记。所以增加了另一个数据结构mod union table解决此问题。

mod union table是一个bit位向量,一个bit表示一个card的状态。

它由新生代垃圾收集器维护,新生代GC将card设置为clean之前,把mod union table设置为dirty。card table状态为dirty、或者mod union table标记为dirty、或者同时两种数据结构都标记为dirty的card表示并发标记阶段引用发生了变化,需要在后面的阶段进行处理。

write barrie

write barrie写屏障类似于一个切面,用户线程写对象引用的时候就触发write barrier的逻辑,将对象所处的card设置为dirty。

并发预清理concurrent preclean

通过参数CMSPrecleaningEnabled选择关闭该阶段,默认启用,主要做两件事情:

1、处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
2、在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(包括ModUnionTalble),通过扫描这些Table,重新标记那些在并发标记阶段引用被更新的对象。

处理dirty card,降低remark阶段暂停时间。

重新标记的过程是STW的,所以为了缩短停顿时间,在并发标记之前应该尽可能多的完成重新标记阶段的工作。并发预清理就是对dirty card进行遍历处理,降低重新标记需要处理的dirty card的数量。

可中断预清理concurrent abortable preclean

该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。

为什么需要这个阶段,存在的价值是什么?

因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。

在该阶段,主要循环的做两件事:

1、处理 From 和 To 区的对象,标记可达的老年代对象
2、和上一个阶段一样,扫描处理Dirty Card和ModUnionTalble中的对象。

当然了,这个逻辑不会一直循环下去,打断这个循环的条件有三个:

1、可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,意思没有循环次数的限制。
2、如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
3、如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。

重新标记final remark

在之前的并行阶段,可能产生新的引用关系如下:

1、老年代的新对象被GC Roots引用
2、老年代的未标记对象被新生代对象引用
3、老年代已标记的对象增加新引用指向老年代其它对象
4、新生代对象指向老年代引用被删除

上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以需要做以下事情:

1、遍历新生代对象,重新标记
2、根据GC Roots,重新标记
3、遍历老年代的Dirty Card和Mod Union Table,重新标记

在第1步骤中,需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少),如果在这之前发生一次YGC,这样就可以避免扫描无效的对象。

CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。

并发清除concurrent sweep

并发清除标记为不可达的对象,回收并合并空闲内存。

并发重置concurrent reset

重新设置CMS相关的各种状态及数据结构,为下一个垃圾收集周期做好准备。

缺点
cpu资源敏感,降低吞吐量
CMS没有运行的时候所有全部cpu资源都供用户线程使用,CMS开始并发运行后就要跟用户线程竞争cpu资源,导致应用线程运行变慢。对于cpu资源非常紧缺的系统,假设只有2核,CMS运行起来后将占用一半的cpu资源,用户线程将感知到运行速度减半。

并发带来的好处是可以降低用户线程的停顿时间,对于在线服务类应用非常有益,因为长时间的停顿可能导致响应超时等问题。但相对于非并发垃圾收集器,CMS整个周期内很多工作是重复的(比如重新标记阶段对dirty card中的对象重新标记,而在并发标记阶段可能已经标记过了),导致整体的吞吐量是降低的。

浮动垃圾

因为CMS垃圾收集器的特性,被标记过的对象,即使最终变成垃圾本次GC也不会回收它,这些垃圾就是浮动垃圾。浮动垃圾的产生意味着内存里不光装着存活对象,还要装着这些浮动垃圾,所以容纳同样多的存活对象CMS需要占用更大的内存空间。

内存碎片

CMS使用标记清除算法,收集结束之后会产生大量内存碎片。当有大对象需要分配空间时,可能总的空间大小是足够的,但是没有连续的空间装下此对象。

CMS默认开启UseCMSCompactAtFullCollection 参数,在FullGC时进行内存碎片的合并整理。内存碎片虽然解决了,但负面影响就是停顿时间变长了。还有另外一个CMSFullGCsBeforeCompaction参数可以控制多少次FullGC才会进行整理,默认是0代表每次FullGC都会进行碎片整理。

运行过程常见问题

concurrent mode failure
并发虽好,但会引入一些问题。对于非并发的垃圾收集器,可以等到老年代无法分配对象时再执行GC。但对于cms则需要预留出空间提前开始GC,预留的空间供并发期间新对象的分配及新生代对象的晋升使用。如果在老年代分配对象发现老年代装不下,则会触发concurrent mode failure,此时将会暂停用户线程执行FullGC或者串行模式的CMS。

promotion failed
这个错误涉及到CMS担保机制,新生代GC之前会根据历史晋升到老年代对象的大小,预估本次老年代是否足够容纳新生代晋升的对象。如果预估时空间足够,但新生代GC实际执行时发现容纳不了,则会引起promotion failed错误。

OutOfMemoryError 
CMS垃圾收集器发现大部分时间都浪费在GC上就会抛出OutOfMemoryError异常,具体为98%的时间在GC但回收不到2%的空间。这样做实际上是为了防止程序进入一种虽然在运行实际上一直在GC假死状态,也可以通过设置-XX:-UseGCOverheadLimit禁用该机制。

原文链接:https://blog.csdn.net/hongxingxiaonan/article/details/105019325

CMS垃圾收集器详解相关推荐

  1. CMS垃圾收集器详解(转载)

    文章目录 概念 CMS的GC过程 初始标记 并发标记 并发预处理 重新标记 并发清除 CMS的缺点 总结: 概念 CMS全称为Concurrent Mark Sweep,即 并发标记清除,对比其他的收 ...

  2. GC之7大垃圾收集器详解(下)

    GC之7大垃圾收集器详解 目录 GC之CMS收集器 GC之SerialOld收集器 GC之如何选择垃圾收集器 GC之G1收集器 第一部分请参见: GC之7大垃圾收集器详解(上) 6. GC之CMS收集 ...

  3. GC之7大垃圾收集器详解(上)

    GC之7大垃圾收集器详解 目录 GC之7大垃圾收集器概述 GC之Serial收集器 GC之ParNew收集器 GC之Parallel收集器 GC之ParallelOld收集器 GC之CMS收集器 GC ...

  4. G1 垃圾收集器详解

    Garbage First(简称G1)收集器开创了收集器面向局部收集的设计思路和基于Region的内存布局形式.它是一款专门针对于拥有多核处理器和大内存的机器的收集器,在满足了GC响应时间的延迟可控的 ...

  5. CMS、G1垃圾收集器详解

    CMS垃圾收集器 基本上只能用在老年代,很长一段时间是ParNew(新生代)搭配CMS(老年代)来使用的. 收集过程大概会分为如下4个过程: 1.初始标记: 暂停所有的其他线程,并记录下gc root ...

  6. JVM垃圾收集器详解 CMS、G1、Shenandoah、ZGC

    上一篇我们讲解了一些垃圾回收的理论和一些基础的算法和思想,这一篇主要是jvm从古至今垃圾收集器的实现. 各垃圾回收器 注:有连线的代表他们可以互相配合使用. Serial和Serial Old收集器 ...

  7. (七)Java垃圾收集器详解

    面试官问:Java垃圾收集器了解过多少,说一下 JVM 有哪些垃圾回收器?这些问题在你面试高级Java的时候经常会问到.本篇文章结合着[深入理解Java虚拟机]一书当中整理了本篇博客. 如果想要对收集 ...

  8. java 垃圾收集器_JVM垃圾收集器详解

    说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物.事实上,GC的历史远比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态 ...

  9. 深入理解Java虚拟机——JVM垃圾回收机制和垃圾收集器详解

    一:概述 说起垃圾回收(Garbage Collection,GC),很多人就会自然而然地把它和Java联系起来.在Java中,程序员不需要去关心内存动态分配和垃圾回收的问题,顾名思义,垃圾回收就是释 ...

最新文章

  1. 注意!你的 Navicat 可能被投毒了...
  2. lisp提取长方形坐标_求修改lisp程序,如何提取CAD中多个点的坐标,(本人想提取UCS坐标系)另外只需要提取X,Y值,不要Z...
  3. 【项目管理】技术债务
  4. python替换文件内容_Python实现替换文件中指定内容的方法
  5. Linux的目录结构与磁盘分区
  6. 一个特殊的List去重问题的解决方案
  7. 利用html制作通讯录_动漫制作软件哪些好用?动漫制作软件推荐
  8. DotNetBar的使用—(Office2007界面风格)
  9. 【预定义】C语言预定义代码(宏、条件编译等)内容介绍【最全的保姆级别教程】
  10. 串口转HID键盘鼠标芯片沁恒微电子CH9329
  11. CSDN上代码块背景颜色的设置
  12. MacBook Air老本重装系统
  13. 短视频开发SDK 架构设计实践
  14. Windows To Go入坑记录
  15. 【软件安装】SPSS22.0安装
  16. 82c55单片机c语言,8255/82C55中文资料简介
  17. annoconda 创建虚拟环境 删除虚拟环境
  18. 为canvas添加背景图片
  19. 一元购宝倒计时之angularJs-$interval
  20. 终于把区块链的技术与应用讲清楚了(57 张 PPT)

热门文章

  1. 使用node-red通过MQTT传送数据
  2. 这款小程序 能让你和孙悟空一样 可以七十二变
  3. 计算机的英文怎么说不是computer,【英语写作】“开电脑”可不是“open the computer”,说错了很尴尬!...
  4. 基于安卓的 gps定位系统的跑步健身助手
  5. 正则表达式中Pattern类、Matcher类和matches()方法简析
  6. 广告联盟的几大防作弊技术
  7. 前后端分离电商B2C模式之_后端_商品评论功能
  8. 从跨境电商到成功转行数据分析师,我拒绝了月薪20K的offer... ##
  9. 【滚动更新】Google退出中国后续报道之二
  10. 【转】嵌入式开发正在日薄西山_有道理哦