Pre
JVM-04垃圾收集Garbage Collection(上)【垃圾对象的判定】
JVM-05垃圾收集Garbage Collection(中)【垃圾收集算法】
JVM-06垃圾收集Garbage Collection(下)【垃圾收集器】
概述
Concurrent Mark Sweep 并发标记清除 。
CMS 收集器是一种以获取最短回收停顿时间为目标的收集器。
它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作 。
从名字( Mark Sweep )上也可以看出 CMS收集器是一种 “标记-清除”算法实现的垃圾收集器。
阶段
初始标记 (STW)
初始标记阶段,会Stop the word , 这个阶段CMS仅标记被GC Root直接引用的对象,所以速度很快。
为什么这个阶段要STW呢?
试想一下,如果初始标记阶段 GC线程和用户线程同时运行,GC的过程中,用户线程还是会有新的垃圾对象产生 ,那啥时候能标记完,徒增复杂。 同时CMS设计的理念就是用户感知至上,所以虽然STW,但也尽量缩短STW的时间,所以选择了仅标记被GC Root直接引用的对象,而无需遍历整个堆 。
并发标记 (用户线程和GC线程并行工作)
经过了上一步的初始标记, 已经将GC Root 直接引用的对象标记完成。
CMS 老年代的Garbage Collector , 回收的是整个堆的垃圾对象,效率随着堆的大小成反比 ( 如果堆比较大,比如10G,CMS也是有点吃力的,所以才有了G1)
为了避免GC时间过长,这个阶段CMS 让用户线程和 GC线程同时工作,尽量减少用户线程的停顿。 GC线程这个时候就要从GC Roots的直接关联对象开始遍历整个对象图,这个耗时最长。 所以CMS重点优化了这块 。 I
用户线程和GC线程并行工作,多少都会存在一些问题 。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
试想一下 GC工作的时候,用户线程也在工作
如果GC完成后,当时遍历的用户线程引用的对象由不是垃圾对象,变成了垃圾对象 ,那是不是就 漏标了?
如果GC线程运行中,当时标记的对象是垃圾对象,但是用户线程运行的过程中又把这个对象重新引用了,那是不是 错标了 ?
怎么办呢? CMS设计了 下个阶段 重新标记 来修复这个阶段因对象状态变更导致的标记错误。
重新标记 (STW)
这个阶段主要是修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。 所以这个时候要STW,不然还是会出现阶段二的情况。
这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
如何重新标记呢? 这里只要采用 三色标记中的增量更新算法。
并发清理(用户线程和GC线程并行工作)
又是并行执行
如果这个清理阶段又有新的对象进来(肯定没有被标记,因为上一步已经标记过了),这个时候怎么办? 也删掉? 还不是垃圾对象啊 删掉那肯定不行。该怎么办呢?
CMS是这样处理的: 对于新增对象会被标记为黑色不做任何处理
这样的对象被称为 【浮动垃圾】, 标记黑色,本次不处理,等下次GC。
并发重置
经过这么一轮的标记,最后的阶段 肯定是把 标记清除掉,等待下一轮 ~
concurrent mode failure 是怎么回事儿
本身full GC 就是因为老年代没有空间了, 在 并发标记 和 并发清除阶段 是 用户线程和GC线程 并行执行, 如果这个时候 用户线程又产生了一些大对象或者符合条件的对象晋升到了老年代, 这个时候 老年代没有空间存放这些对象了,GC一边回收,系统一边运行,也许没回收完就再次触发full gc , 就出现了 “concurrent mode failure” .
出现这个情况,CMS是怎么处理呢? 直接OOM?
显然不是,CMS会Stop the word , 然后使用Serial Old 单线程 来进行垃圾回收。 尽量避免这种情况,效率非常低。
如何避免呢? 可以合理设置CMS的参数 (-XX:CMSInitiatingOccupancyFraction) 默认92% ,可以将这个值设置为80%(根据业务考量该值,你的系统大对象多的话 ,当然了,你设置了 80% 就意味着你老年代将会有20%的空间不可用,这部分空间仅能用来存放GC过程中新的对象,需要合理评估),尽量避免并发失败的情况的发生。
CMS核心参数
-XX:+UseConcMarkSweepGC:启用cms
-XX:ConcGCThreads:并发的GC线程数
-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理,其目的是为了减少内存碎片
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92%)
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW的时间
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW的时间
CMS的优缺点
优点
- 并发收集
- 低停顿 ,用户体验至上
缺点:
- 用户线程和GC线程有可能争抢CPU资源
- 无法处理浮动垃圾 , 在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次gc再清理了
- 回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生, 可以通过参数-XX:+UseCMSCompactAtFullCollection让jvm在执行完标记清除后再做整理,从而实现“标记-整理”的效果,减少内存碎片。 还有个参数 -XX:CMSFullGCsBeforeCompaction 代表多少次Full GC以后整理一下内存碎片,默认为0 即每次Full GC之后都会整理内存碎片。
- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况 。 比如在并发标记和并发清理阶段会出现concurrent mode failure,这个时候会STW,会切换到Serial Old ,效率非常低。
ParNew + CMS设置Demo
-Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
没有标准答案,需要依据自己的业务特点,进行合理的参数调优。
GC算法底层实现
三色标记
在并发标记的过程中,应用线程也在运行,所以GC线程在运行期间应用线程使用的对象之间的引用可能发生变化,会存在多标和漏标的现象。
JVM把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色 : 黑 灰 白
黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。