CMS概述
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间
为目标的收集器。
CMS收集器主要用于要求低延迟(即:提高响应速度)的互联网项目。
设置CMS收集器参数:-XX:+UseConcMarkSweepGC。
采用的是"标记-清除算法",整个过程分为4步
(1)初始标记 CMS initial mark 标记GC Roots直接关联对象,不用Tracing,速度很快
(2)并发标记 CMS concurrent mark 进行GC Roots Tracing
(3)重新标记 CMS remark 修改并发标记因用户程序变动的内容
(4)并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理称为浮动垃圾
为什么我的CMS回收流程图上初始标记是单线程,为什么不使用多线程呢?
初始化标记阶段是串行的,这是JDK7的行为。JDK8以后默认是并行的,可以通过参数
-XX:+CMSParallelInitialMarkEnabled控制
这是因为在JDK7以前应用程序线程大多使用的是一个或者两个cpu核心,在这种情况下如果使用多线程进行初始标记,线程的上下文频繁切换会增加额外的开销,还不如直接使用单线程进行初始标记,效率更高
而JDK8以后应用程序线程大多是多个CPU核心了,所以使用多线程的初始标记,可以提高效率
而重新标记的时候,由于我的并发标记阶段已经开启了多个线程,所以可以直接使用多线程进行重新标记,没有额外的线程开销,所以重新标记阶段使用的是多线程
CMS的两种模式与一种特殊策略
Backgroud CMS
实际上我们的并发标记还能被整理成两个流程
(1)初始标记
(2)并发标记
(3)并发预处理
(4)可中止的预处理
(5)重新标记
(6)并发清除
为什么我们的并发标记细化之后还会额外有两个流程出现呢?
讨论这个问题之前,我们先思考一个问题,假设CMS要进行老年代的垃圾回收,我们如何判断被年轻代的对象引用的老年代对象是可达对象。
也就是这张图,当老年代被回收的时候,我们如何判断A对象是存活对象。
答:必须扫描新生代来确定,所以CMS虽然是老年代的垃圾回收器,却需要扫描新生代的原因。
问题2:既然这个时候我需要扫描新生代,那么全量扫描会不会很慢
答:肯定会的 ,但是接踵而来的问题:既然会很慢,我们的停顿时间很长,可是CMS的目标是什么
CMS(Concurrent Mark Sweep)收集器是一种以获取 最短回收停顿时间
为目标的收集器。这不是与他的设计理念不一致吗?
思考:怎么让我们的回收变快
答:肯定是垃圾越少越快。所以我们的CMS想到了一种方式,就是我先进行新生代的垃圾回收,也就是一次young GC,回收完毕之后。是不是我们新生代的对象就变少了,那么我再进行垃圾回收,是不是就变快了。
所以,CMS有两个参数:
CMSScheduleRemarkEdenSizeThreshold 默认值:2M
CMSScheduleRemarkEdenPenetration 默认值:50%
Foregroud CMS
其实这个也是CMS一种收集模式,但是他是并发失败才会走的模式,简单来说,也就是我去进行并发标记的时候,内存不够了,这个时候我会进入STW,并且开始全局Full GC
-XX:CMSInitiatingOccupancyFraction
在使用CMS收集器的情况下,指定老年代被使用的内存空间的阈值,达到该阈值则触发Full GC。
-XX:+UseCMSInitiatingOccupancyOnly
指定用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction参数的值),如果不指定,JVM仅在第一次使用设定值,后续则会根据运行时采集的数据做自动调整,如果指定了该参数,那么每次JVM都会在到达规定设定值时才进行GC。不过大多数情况下,JVM都能够作出更好的垃圾收集决策,所以如果不是很有信心的话,不建议使用该参数,放心的把决定权交给JVM。
按照默认值 当老年代达到 92 %时,会触发CMS回收。
CMS的标记压缩算法-----MSC(Mark Sweep Compact)
他的回收方式其实就是我们的滑动整理,并且进行整理的时候一般都是两个参数
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
这两个参数表示多少次FullGC后采用MSC算法压缩堆内存,0表示每次FullGC后都会压缩,同时0也是默认值
碎片问题也是CMS采用的标记清理算法最让人诟病的地方:Backgroud CMS采用的标记清理算法会导致内存碎片问题,从而埋下发生FullGC导致长时间STW的隐患。
所以如果触发了FullGC,无论是否会采用MSC算法压缩堆,那都是ParNew+CMS组合非常糟糕的情况。因为这个时候并发模式已经搞不定了,而且整个过程单线程,完全STW,可能会压缩堆(是否压缩堆通过上面两个参数控制),真的不能再糟糕了!想象如果这时候业务量比较大,由于FullGC导致服务完全暂停几秒钟,甚至上10秒,对用户体验影响得多大。
三色标记
在并发标记阶段,由于标记期间与应用程序并行,对象间的引用关系可能发生变化,因此采用三色标记的方式对对象进行标记,标记过程分为三种颜色:白色、灰色、黑色。
黑色:表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
灰色:表示对象已经被垃圾收集器访问过, 但该对象上至少存在一个引用还没有被扫描过。
白色:表示对象尚未被垃圾收集器访问过。 在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
标记过程
1)初始时,所有对象都在 【白色集合】中。
2)将GC Roots直接引用到的对象转移【灰色集合】中。
3)从灰色集合中获取对象:
将本对象引用到的其他对象全部转移【灰色集合】中。
将本对象转移【黑色集合】中。
三色标级存在漏标和多标问题。
漏标
漏标只有同时满足以下两个条件时才会发生:
条件一:灰色对象 断开了 白色对象的引用;即灰色对象 原来成员变量的引用 发生了变化。
条件二:黑色对象 重新引用了 该白色对象;即黑色对象 成员变量增加了 新的引用。
如图所示:
图中,有ABCD四个对象,A依赖B和C,C依赖D,初始标记完成后A对象已经被扫描过标为灰色,其他对象为白色;继续扫描B和C,当B和C扫描完后,A变成了黑色,B变成了灰色,C是黑色,D还是白色。此时如果应用程序将B和D的引用去掉,让C依赖D,建立起C和D的关系之后B变成了黑色。此时问题产生了,C已经是黑色,不会再对其依赖对象进行扫描,但事实上C还有一个依赖对象D没有被扫描。如果进行垃圾回收,D会被回收掉,这就是漏标问题。
漏标解决方案
增量更新:就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向白色对象的引用之后, 它就变回灰色对象了。
多标
如图所示:
图中,有ABCD四个对象,AB是黑色,C是灰色,D是白色,当GC正在扫描D时,B被置空了,此时B理应被回收,但是因为GC不会对黑色对象做重复扫描,所以B还是黑色,在进行垃圾清理时不会被回收,只能等下次GC时再进行重新标记扫描。这种情况相对于漏标来说不会导致系统出BUG,这部分本应该回收但是没有回收到的内存,被称之为“浮动 垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。
并发预处理
并发预清理主要是处理并发阶段因引用关系发生变更而未标记到的存活对象(即:扫描所有标记为Direty的Card )。
可终止预处理
可终止预处理阶段与并发预处理节点一样,主要是处理并发阶段因引用关系发生变更而未标记到的存活对象(即:扫描所有标记为Direty的Card )。但是可终止预处理是有条件触发的,触发条件由CMS的两个参数控制:
-
参数CMSScheduleRemarkEdenSizeThreshold,默认值:2M。(Threshold:门槛)
-
参数CMSScheduleRemarkEdenPenetration,默认值:50%。(Penetration:占有率)
这两个参数一般是组合使用,即:当Eden空间使用超过2M时,启动可终止预处理,当Eden空间使用率到达50%时中断,进入重新标记阶段。
同时,CMS提供了一个参数CMSMaxAbortablePrecleanTime (默认为5S),表示不管Eden空间使用率是否到达参数CMSScheduleRemarkEdenPenetration配置的值,都会中断,进入重新标记阶段。
最后,CMS还提供参数CMSScavengeBeforeRemark(Scavenge:清扫)(默认关闭,建议开启,开启方式:-XX:+CMSScavengeBeforeRemark),表示进入重新标记前强行执行一次Minor GC。
记忆集
当我们进行young gc时,我们的gc roots除了常见的栈引用、静态变量、常量、锁对象、class对象这些常见的之外,如果 老年代有对象引用了我们的新生代对象 ,那么老年代的对象也应该加入gc roots的范围中,但是如果每次进行young gc我们都需要扫描一次老年代的话,那我们进行垃圾回收的代价实在是太大了,因此我们引入了一种叫做记忆集的抽象数据结构来记录这种引用关系。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构。
卡表
记忆集是我们针对于跨代引用问题提出的思想,而卡表则是针对于该种思想的具体实现。(可以理解为记忆集是结构,卡表是实现类)
(1) 卡表是使用一个字节数组实现:CARD_TABLE[],每个元素对应着其标识的内存区域一块特定大小的内存块,称为"卡页"。hotSpot使用的卡页是2^9大小,即512字节
(2) 一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0。GC时,只要筛选本收集区的卡表中变脏的元素加入GC Roots里。
并发标记的时候,A对象发生了所在的引用发生了变化,所以A对象所在的块被标记为脏卡
继续往下到了重新标记阶段,修改对象的引用,同时清除脏卡标记。
CMS的线程数计算公式
-XX:ParallelGCThreads
-XX:ParallelGCThreads=m // STW暂停时使用的GC线程数,一般用满CPU
其中ParallelGCThreads 参数的默认值是:
CPU核心数 <= 8,则为 ParallelGCThreads=CPU核心数,比如4C8G取4,8C16G取8
CPU核心数 > 8,则为 ParallelGCThreads = CPU核心数 * 5/8 + 3 向下取整
-XX:ConcGCThreads
-XX:ConcGCThreads=n // GC线程和业务线程并发执行时使用的GC线程数,一般较小
ConcGCThreads的默认值则为:
ConcGCThreads = (ParallelGCThreads + 3)/4 向下取整