随笔分类
CMS
低延迟
并发执行
在JDK 1.5 时期,Hotspot推出了一款在强交互应用 中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作.
CMS收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间
. 停顿时间越短(低延迟)就越适合与用户进行交互,良好的响应速度能够提升用户体验
-
目前很大一部分的Java应用几乎都在互联网网站以及B/S系统的服务器上,这类应用尤其重视服务的响应速度,希望系统停顿时间尽可能短,以来给用户良好的体验
CMS收集器就非常符合这类应用的需求
-
CMS的垃圾收集采用了
标记-清除
算法,并且也会"Stop-the-World".
不幸的是,CMS 作为老年代的收集器,却无法与 JDK 1.4.0 中已经存在的新生代收集器Parallel Scavenge 配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC
过程
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段 以及并发清除阶段
- 初始标记(Initial-Mark):在这个过程中,程序中所有的工作线程都将会因为"Stop-the-World"机制而出现短暂的暂停,这个阶段的主要任务
仅仅标出GC Roots能直接关联到的对象
. 一旦标记完成之后就会恢复之前被暂停的所有应用线程,由于直接关联的对象比较少,所以这里的暂停时间实际上是十分短暂的 - 并发标记(Concurrent-Mark):从GC Roots的
直接关联对象开始遍历整个对象图
的过程,这个过程耗时较长
但是不需要停顿用户线程
,可以与垃圾收集线程一起并发运行 -> 但其本身是需要占用一部分用户线程的 - 重新标记(Remark):由于在并发标记过程中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了
修整并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录(重新标记实际上标记为垃圾对象的纠正,因为并发标记过程中可能存在不可达对象复活事件)
,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但是也远比并发标记阶段的时间短,因为这个过程只是对标记为垃圾对象
做一些校正工作
而已,并不能够来处理浮动垃圾
- 并发清除(Concurrent-Sweep):此阶段将会去清除标记阶段判断的已经死亡的对象,释放内存对象. 由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
总结
上述可见,仅有初始标记以及重新标记两个阶段设计到了Stop-the-World,可见CMS当之无愧真正意义上的第一款并发的垃圾收集器.
再论
尽管CMS收集器采用的是并发回收(非独占式),但是在其初始标记
和重新标记(校正)
这两个阶段仍然需要执行Stop -the-World机制暂停程序中的工作线程,不过暂停时间并不会太长,但这也说明了目前所有的垃圾收集器都做不到完全不需要"Stop-the-World",只是尽可能地缩短暂停时间
由于最耗时间的并发标记和并发清理阶段不需要暂停工作,所以整体的回收是低停顿的
.
另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,需确保应用程序用户线程有足够的内存可用
. 因此,CMS收集器不能像其它收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收
,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行.
要是CMS运行期间预留的内存无法满足程序需要,就会出现一次"Concurrent Mode Failure
" 失败,这是虚拟机将启动后备预案
:临时启用 Serial Old
收集器来重新进行老年代的垃圾收集,这时,停顿时间明显增长了.
CMS收集器的垃圾收集算法采用的是标记-清除
算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片
. 那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)
执行内存分配
CMS中使用Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact?
- CMS中清除阶段是并发执行的,如果使用Compact整理内存,原来用户线程使用的内存将无法使用!要保证用户线程能继续执行,前提是其运行时所需、所用资源不受影响
- Mark-Compact更适合"Stop-the-World"这种场景下来使用
评价
CMS
-
优点:
- 并发收集
- 低延迟
-
弊端
-
会产生内存碎片,导致并发清除好,用户线程可用的空间不足. 在无法分配大对象的情况下,不得不提前触发Full GC,但可以来设置触发几次Full GC后执行一次内存的压缩,以来得到规整的内存.
-
CMS对CPU资源十分敏感. 在并发阶段,它虽然不会导致用户停顿,但其会
占用一部分用户线程
而导致应用程序变慢,总的吞吐量会降低
. -
CMS无法来处理浮动垃圾. 可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生. 在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或交叉运行的,那么
在并发标记阶段如果产生新的垃圾对象,CMS是无法对这些新的垃圾对象进行标记的,最终会导致这些新的垃圾对象没有得到及时回收
,从而只能是在下一次执行GC时释放这些之前未被回收的内存空间.
官网描述重新标记
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html
The second pause comes at the end of the concurrent tracing phase and finds objects that were missed by the concurrent tracing due to updates by the application threads of references in an object after the CMS collector had finished tracing that object. This second pause is referred to as the remark pause.
-
翻译:第二次暂停是在并发跟踪阶段结束时进行的,它查找由于CMS收集器完成对对象的引用后,应用程序线程对对象中的引用进行更新而导致并发跟踪遗漏的对象。该第二暂停称为重新标记暂停。
标记的是不可达的对象,并发标记阶段新生的垃圾并没有得到标记,重新标记也只是对已标记的垃圾对象的校正而已,因为我们知道,对存活的对象进行GC是严重的错误.
并发标记过程中可能会出现的情况:
-
可达的对象,变为了不可达
-
本来不可达的内存,变得可达了
第一种情况便是"
浮动垃圾
",而第二种也便是重新标记
阶段所要校正的标记工作了.比如说,如果并发标记阶段
new
了一个对象,那么它在初始标记和并发阶段是不会能够从GC Roots
标记为可达的,这也就是所谓的were missed
,如果没有重新标记阶段来将这个对象标记为可达,那么其在并发清理阶段就会被清理,这是严重的错误,不可容忍的操作,因此需要重新标记
来进行相关处理,这实际上也是重新标记阶段实际上的任务了(Remark阶段就是来标记可达的对象) --> 这个例子举得不太严谨,待日后进行纠正.相比之下,
浮动垃圾
是可容忍的问题了. 为什么重新标记阶段不进行浮动垃圾的处理呢? 个人感觉是由可达变为不可达
这个变化需要从GC Roots开始遍历,这相当于再来完成一次初始标记和并发标记的工作,这显得前两个工作时多余的,这大大增加了重新标记阶段的开销,所带来的暂停时间也是追求低延迟的CMS所不能容忍的.
参数设置
-
-XX∶+UseConcMarkSweepGC 手动指定使用CMS 收集器执行内存回收任务
- 开启该参数后会默认将-XX:+UseParNewGC 参数也打开,二者配合使用,当出现 "Concurrent Mode Failure"时,默认会启动 -XX:+UseSerialOldGC来对老年代进行相关清理工作.
-
-XX∶CMSlnitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
- JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS 回收. JDK6及以上版本默认值为92
- 如果内存增长缓慢,可将阈值设置得稍大一些,大的阈值可以有效降低CMS的触发频率
- 如果内存增长迅速,应将阈值设置得小一些,避免出现"Concurrent Mode Failure"的情况,不然有可能会频繁触发老年代的串行收集器.
- 可见,有效的设置该值可以有效降低Full GC的次数.
-
-XX∶+UseCMSCompactatFullCollection 用于指定在执行完Full GC后对内存空间进行压缩整理,以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行,所带来的问题就是停顿时间变得更长了
- 注:CMS造成的内存不规整以及浮动垃圾过多都有可能会触发Full GC.
-
-XX∶CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理
-
-XX∶ParallelCMSThreads 设置CMS的线程数量。
- CMS 默认启动的线程数是(ParallelGCThreads+3)/4,ParallelGCThreads 是年轻代并行收集器的线程数。当CPU 资源比较紧张时,受到CMS收集器线程的影响,应用程序的性能在垃圾回收阶段可能会非常糟糕
小结
HotSpot有那么多的垃圾收集器,其各自的应用场景又该如何去区分?
Serial GC、Parallel GC、Concurrent Mark GC三者有何区别?
- 如果想最小化地使用内存和并行开销,选择Serial GC
- 如果想最大化程序的吞吐量,选择Parallel GC
- 如果想最小化GC的中断以及停顿时间,选择CMS
JDK后续版本CMS的变化