一、简介

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. DataGear 变更部署数据库为SQL Server填坑指南(含转写后的SQL server代码及SQL server配置文件)

    1. 引言 2. 配置数据库链接 3. 引入数据库驱动 4. 手动初始化数据库 5. 改写SQL 6. 其他 7. 参考 1. 引言 DataGear默认使用Derby数据库作为系统的元数据库,至于待 ...

  2. .NET平台系列10 .NET统一平台愿景

    系列目录     [已更新最新开发文章,点击查看详细] 2019年,微软分享了[统一的.NET堆栈和生态系统的愿景].给开发者带来的价值是,将能够使用一组API,语言和工具来针对广泛的应用程序类型,包 ...

  3. 网速测试利器-iperf3

    网速测试利器-iperf3 使用工具   简介 iperf3是一个网络速度测试工具,支持IPv4与IPv6,支持TCP.UDP.SCTP传输协议,可在Windows.Mac OS X.Linux.Fr ...

  4. 华为交换机Console口属性配置

    华为交换机Console口属性配置 一.设置通过账号和密码(AAA验证)登陆Console口 进入 Console 用户界面视图 <Huawei>system-view [Huawei]u ...

  5. 2.9. 管道和重定向ls /proc && echo suss! || echo failed. 能够提示命名是否执行成功or失败; 与上述相同效果的是: if ls /proc; then echo suss; else echo fail; fi

    2.9. 管道和重定向 批处理命令连接执行,使用 | 串联: 使用分号 ; 前面成功,则执行后面一条,否则,不执行:&& 前面失败,则后一条执行: || ls /proc && ...

  6. VMware安装RedHat7、CentOS7后无网卡解决办法

    由于Vmware虚拟网卡和linux兼容问题导致驱动无法正常安装,默认的网卡类型不兼容找到我们的Vmware虚拟机文件夹,将VMware 虚拟机配置 (.vmx),追加一条设置,网卡类型etherne ...

  7. Linux服务之cobbler批量部署篇

    一.Cobbler简介:Cobbler通过将设置和管理一个安装服务器所涉及的任务集中在一起,从而简化了系统配置.相当于Cobbler封装了DHCP.TFTP.XINTED等服务,结合了PXE.kick ...

  8. Java Stream 流(JDK 8 新特性)

    什么是 Steam Java 8 中新增了 Stream(流)来简化集合类的使用,Stream 本质上是个接口,接口中定义了很多对 Stream 对象的操作. 我们知道,Java 中 List 和 S ...

  9. GPIO端口上拉下拉 与 硬件图的上拉下拉

    硬件图上的上拉下拉: 没有触发时默认接到IO的是高电平就是上拉: 没有触发时默认接到IO的是低电平就是 下拉: (2)对应GPIO的配置 配置与你的外围电路息息相关: 比如下图: 你只能配置为上拉: ...

  10. docker swarm外部验证负载均衡时不生效

    问题描述 我在本地创建了3个装了centos7的虚拟机, 并初始化了swarm集群, 即1个manager节点, 2个worker节点; 三台机子的ip分别是 192.168.124.8 - (man ...