一、简介

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. SpringBoot 上传文件如何获取项目工程路径

    上传文件时,需要将上传的文件存放于工程路径中,以便前端能够获取文件资源,那如何获取工程路径呢? //获取 SpringBoot 工程中 static 的绝对路径 String serverpath= ...

  2. VMWare虚拟机显示模块“Disk”启动失败

    找到启动虚拟机的目录: 在此路径中找到.vmx文件,在文件中查找(Ctrl+F快速查找)vmci0.present,此时会看到"vmci0.present = "TRUE" ...

  3. Centos7 Samba共享服务搭建

    Centos7 Samba共享服务搭建 1.安装启动和端口 ---------------------------------------------------------------------- ...

  4. Linux下RAID磁盘阵列的原理与搭建

    RAID概念 磁盘阵列(Redundant Arrays of Independent Disks,RAID),有"独立磁盘构成的具有冗余能力的阵列"之意. 磁盘阵列是由很多价格较 ...

  5. 如何让Android 支持HEIF 图片解码和加载(免费的方法)

    字节跳动火山引擎ImageX提供了一种能力,可以支持客户端android 直接解码HEIF 和HEIC图片,经过测试发现,可以免费使用: 一.阅前准备 HEIF图片格式是什么? 高效率图像格式(Hig ...

  6. (转)修改python默认排序方式

    在Java中,自定义类可以通过继承comparable接口,重写compareTo方法来使用内置sort()函数来对自定义对象排序,我就在想Python中有没有类似的操作. 首先随便写个自定义类,比如 ...

  7. NVIDIA GPU上的随机数生成

    NVIDIA GPU上的随机数生成 NVIDIA CUDA随机数生成库(cuRAND)提供高性能的GPU加速的随机数生成(RNG).cuRAND库使用NVIDIA GPU中提供的数百个处理器内核,将质 ...

  8. SLAM相机定位

    SLAM相机定位 摘要 深度学习在相机定位方面取得了很好的结果,但是当前的单幅图像定位技术通常会缺乏鲁棒性,从而导致较大的离群值.在某种程度上,这已通过序列的(多图像)或几何约束方法解决,这些方法可以 ...

  9. 深度学习LiDAR定位:L3-Net

    深度学习LiDAR定位:L3-Net 摘要 本文提出L3-Net--一种新颖的基于学习的LiDAR定位系统,可实现厘米级的定位,与现有最高水平的传统定位算法相媲美.与传统定位算法不同,本文创新地实现了 ...

  10. 编写HSA内核

    编写HSA内核 介绍 HSA提供类似于OpenCL的执行模型.指令由一组硬件线程并行执行.在某种程度上,这类似于 单指令多数据(SIMD)模型,但具有这样的便利:细粒度调度对于程序员而言是隐藏的,而不 ...