一、简介

CMS垃圾收集器是一款用于老年代的,使用复制-清除-整理算法的垃圾收集器。

二、GC阶段

1、初始化标记(STW)

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

2、并发标记

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

当 GC线程 进行并发操作时,应用程序可能会进行新增对象、删除对象、变更对象引用等一系列操作。这种条件下可能会出现活动对象的漏标的情况,比如下面场景:

  1. A 是活动对象,A->B,标记 B 可达,将其压入标记栈,此时 A 所有直接子对象遍历完,A 出栈,标记线程将不会再访问 A。
  2. 同时应用程序移掉了 B 对 C 的引用,让A重新引用 C。
  3. B 出栈时无法标记 C 可达,A 虽然引用 C 但标记线程不会再访问A,此时 C 会被当成不可达对象。

所以单纯的并发标记操作并不能保证 GC 的正确性,所以还需要额外的操作,这个操作就是 Write Barrier

Write Barrier 就是当改写一个引用时:A.x = C,执行一些额外操作。如果是上面场景可以假设为:

write_barrier(obj, field, newobj){
if(newobj.mark == FALSE)
newobj.mark = TRUE
push(newobj, $mark_stack)
*field = newobj
}

即当赋值引用时,如果赋值的对象还没有被标记,将标记该对象将其压入标记栈。在使用 Write Bariier 之后同样的情景就不会出现活动对象被遗漏的情况了。

所以我们知道并发标记阶段,并不是只有单纯的并发标记,还有一个额外的 Write Bariier 操作,避免活动对象被漏标。

3、重新标记(STW)

重新标记可以理解成一个同步刷新对象间引用的操作,整个过程是 STW。在并发标记其间,应用程序不断变更对象引用,此时的 GC ROOTS 有可能会发生变化,这个时候需要同步更新这个增量变化。于是重新从当前的 GC ROOTS 和指针更新的区域出发(mod-union table)再进行一次标记,所以这个过程被叫作重新标记。需要注意的是:已经标记的对象是不会再遍历一次,标记线程识别对象在并发阶段已经标记过了,就会跳过该对象。所以重新标记只会遍历那些新增没有标记过的活动对象和其间有指针更新的活动对象,如果指针更新频繁,重新标记很有可能会遍历新生代中的大部分甚至全部对象。所以如果重新标记阶段很慢,可以启动一次YGC,来减少并发标记的工作量减少其停顿时间。

4、并发清除

重新标记结束后,应用程序继续运行,此时分出一个处理器去进行垃圾回收工作。

老年代的对象通常是存活时间长,回收比例低,所以采用的回收算法是标记-清除。这个阶段 GC 回收线程是遍历整个老年代,遇到没有被标记的对象(垃圾)就清空掉相应的内存块,并加入可分配列表。遇到被标记的对象保持原来的位置不动,只是重置其标记位,用于下一次GC。

5、并发重置

Oracle 官方文档中描述这个阶段的工作是:重新调整堆的大小,并为下一次GC做好数据结构支持,比如重置卡表的标位,具体细节有待考证。

三、知识点

1、卡表

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

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

2、Concurrent Mode Failure

如果 CMS 在清理掉垃圾对象之前,老年代中没有足够的空间存放新产生的对象,就会出现 Concurrent Mode Failure,

四、缺点

  1. 内存碎片(原因是采用了标记-清除算法)
  2. 对 CPU 资源敏感(原因是并发时和用户线程一起抢占 CPU)
  3. 浮动垃圾:在并发标记阶段产生了新垃圾不会被及时回收,而是只能等到下一次GC

然后我产生了一个疑问:既然重新标记可以修正并发标记阶段的变动,那么为何还有浮动垃圾问题?

由于标记阶段是从 GC Roots 开始标记可达对象,那么在并发标记阶段可能产生两种变动:

  1. 本来可达的对象,变得不可达了。(浮动垃圾)
  2. 本来不可达的内存,变得可达了。

浮动垃圾是可容忍的问题,而不是错误。那么为什么重新标记阶段不处理第一种变动呢?也许是由可达变为不可达这样的变化需要重新从 GC Roots 开始遍历,相当于再完成一次初始标记和并发标记的工作,这样不仅前两个阶段变成多余的,浪费了开销浪费,还会大大增加重新标记阶段的开销,所带来的暂停时间是追求低延迟的CMS所不能容忍的。

四、日志解读

# 年轻代 GC,使用 ParNew
0.210 [GC (Allocation Failure) [ParNew: 279616K->34942K(314560K), 0.0246537 secs] 279616K->79707K(1013632K), 0.0246999 secs] [Times: user=0.06 sys=0.09, real=0.03 secs] # 老年代 GC # 初始计标记,速度很快只用了3.1毫秒
# 372071K:老年代的使用量;699072K:老年代的总容量;413038K:堆的使用量;1013632K:整个堆的容量;0.0003105 secs:耗时
0.526: [GC (CMS Initial Mark) [1 CMS-initial-mark: 372071K(699072K)] 413038K(1013632K), 0.0003105 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] # 启动并发标记步骤
0.526: [CMS-concurrent-mark-start]
0.529: [CMS-concurrent-mark: 0.003/0.003 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] # 启动预清理步骤,这个阶段会尽可能在重新标记前,处理掉一些在并发标记阶段发生变化的引用关系,从而降低重新标记阶段的停顿时间
# 清理 Eden 区中发生变化的引用(dirty card)
0.529: [CMS-concurrent-preclean-start]
0.530: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] # 启动可中断的预清理,这个阶段主要处理 from 和 to 区域对象引用 old gen 的变化,同样也会继续处理 dirty card 的对象引用。这个阶段默认设置的时间是5s,如果执行逻辑超过5s,会自动终止这个阶段,或者当eden区使用内存值小于 CMSScheduleRemarkEdenPenetration,默认 50% 时,也会退出这个阶段。
0.530: [CMS-concurrent-abortable-preclean-start]
0.844: [CMS-concurrent-abortable-preclean: 0.006/0.315 secs] [Times: user=1.34 sys=0.12, real=0.31 secs] # 重新标记
# 35015K:年轻代当前的使用量;314560K:年轻代的总容量;
0.845: [GC (CMS Final Remark) [YG occupancy: 35015 K (314560 K)]
# Rescan:进行重新标记,耗时 0.0004597 secs
0.845: [Rescan (parallel) , 0.0004597 secs]
# 处理弱引用
0.845: [weak refs processing, 0.0000086 secs]
# 卸载 class
0.845: [class unloading, 0.0004208 secs]
# 清理类级元数据和内部化字符串的符号和字符串表
0.845: [scrub symbol table, 0.0004006 secs]
0.846: [scrub string table, 0.0001479 secs]
# 老年代的使用情况及堆的使用情况
[1 CMS-remark: 677072K(699072K)] 712088K(1013632K), 0.0014893 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] # 启动并发清除,任务是清除那些没有标记的无用对象并回收内存
0.846: [CMS-concurrent-sweep-start]
0.847: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] # 启动并发重置,作用是重新设置CMS算法内部的数据结构
0.847: [CMS-concurrent-reset-start]
0.849: [CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

垃圾处理器-CMS的更多相关文章

  1. JVM调优之垃圾定位、垃圾回收算法、垃圾处理器对比

    谈垃圾回收器之前,要先讲讲垃圾回收算法,以及JVM对垃圾的认定策略,JVM垃圾回收器是垃圾回收算法的具体实现,了解了前面的前置知识,有利于对垃圾回收器的理解. 什么是垃圾? 垃圾,主要是指堆上的对象, ...

  2. CMS前世今生

    CMS一直是面试中的常考点,今天我们用通俗易懂的语言简单介绍下. 垃圾回收器为什么要分区分代? 如上图:JVM虚拟机将堆内存区域分代了,先生代是朝生夕死的区域,老年代是老不死的区域,不同的年代对象有不 ...

  3. CMS模板应用调研问卷

    截止目前,已经有数十家网站与我们合作,进行了MIP化改造,在搜索结果页也能看到"闪电标"的出现.除了改造方面的问题,MIP项目组被问到最多的就是:我用了wordpress,我用了织 ...

  4. Kooboo CMS技术文档之五:站点配置管理

    站点关系 管理站点间的关系,站点可以有子站点,子站点继承父站点的部分配置数据,同时子站点还可以根据需要,本地化由父站点继承而来的数据.通过继承和本地化,可以让子站点在用最小的改动代价,来完成一个与父站 ...

  5. Kooboo CMS技术文档之二:Kooboo CMS的安装步骤

    在IIS上安装Kooboo CMS Kooboo CMS安装之后 安装的常见问题 1. 在IIS上安装Kooboo CMS Kooboo CMS部署到正式环境相当简单,安装过程是一个普通MVC站点在I ...

  6. Kooboo CMS技术文档之一:Kooboo CMS技术背景

    语言平台 依赖注入方案 存储模型 1. 语言平台 Kooboo CMS基于.NET Framework 4.x,.NET Framework 4.x的一些技术特性成为站点开发人员使用Kooboo CM ...

  7. Kooboo CMS技术文档之四:Kooboo CMS的站点组成部分

    Kooboo CMS本着功能独立分离的原则,将站点分为三部分组成:用户管理,站点管理和内容数据库管理.各个功能之间既可独立使用,也可以容易组成在一起形成一个完整的系统. 用户管理 管理整个系统内的用户 ...

  8. Kooboo CMS技术文档之三:切换数据存储方式

    切换数据存储方式包括以下几种: 将文本内容存储在SqlServer.MySQL.MongoDB等数据库中 将站点配置信息存储在数据库中 将后台用户信息存储在数据库中 将会员信息存储在数据库中 将图片. ...

  9. zerojs! 造出最好的 CMS 轮子

    zerojs是一个基于nodejs.angularjs.git的CMS.在它之上可以继续开发出博客.论坛.wiki等类似的内容管理型系统. 拥抱开发者和社区 层次清晰,高度解耦.前后端即使分开也都是完 ...

随机推荐

  1. prometheus node-exporter增加新的自定义监控项

    项目中collector中新增加自己所需监控项即可 定义启动node-exporter是传入的参数 var ( phpEndPoint = kingpin.Flag("collector.p ...

  2. [DB] Hadoop免密登录原理及设置

    情景: 现有两台电脑bigdata111.bigdata112,bigdata111想免密码登录bigdata112 过程: 1.bigdata111生成公钥(用于加密,给别人)和私钥(用于解密,自己 ...

  3. [Linux] Linux C编程一站式学习 Part.2

    C语言本质 计算机中数的表示 浮点数:符号位+指数部分(2的多少次方)+尾数部分(小数点后的数字) 用偏移的指数(Biased Exponent)表示负指数 正规化(Normalize):尾数部分最高 ...

  4. 029. Python多态介绍

    多态:不同的子类对象,调用相同的父类方法,产生不同的结果 继承 重写 在不改变原有代码的前提下,实现了不同的效果 class Soldier(): # 攻击 def attack(self): pas ...

  5. 什么是FOC

    https://zhidao.baidu.com/question/354536332.html FOC简述 磁场定向控制系统(FOC)又称为矢量控制系统,他是选择电机某一旋转磁场轴作为特定的同步旋转 ...

  6. 第4讲 | DHCP与PXE:IP是怎么来的,又是怎么没的?

    第4讲 | DHCP与PXE:IP是怎么来的,又是怎么没的? linux 配置网络IP地址: 使用 net-tools: sudo ifconfig eth1 10.0.0.1/24 sudo ifc ...

  7. Django部署uwsgi 与 nginx配置

    1.nginx文件的配置 路径:/etc/nginx/conf.d/ example.conf 启动:service nginx [start]/[restart]/[stop] upstream d ...

  8. 微信小程序使用同声传译实现语音识别功能

    我使用同声传译语音识别功能是为了实现微信小程序首页的语音搜索功能,如果你也是那么恭喜你,你可以ctrl+c.ctrl+v再改一改,如果你不是那么你也不要着急的走可以看完我的文章会对你有所帮助! 首先是 ...

  9. SQL Server将同一列多条数据合并成一行

    Sql server中,将同一字段多条数据用字符拼接为一个字符串方式. 原数据查询展示: 使用 STUFF 函数,将结果列拼接成一行.结果如下: STUFF: 1.作用 stuff(param1, s ...

  10. C/C++语言编程的隐患!

    C/C++语言编程的隐患! 本文将带您了解一些良好的和内存相关的编码实践,以将内存错误保持在控制范围内.内存错误是 C 和 C++ 编程的祸根:它们很普遍,认识其严重性已有二十多年,但始终没有彻底解决 ...