以获取最短回收停顿时间为目标
场景:目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求
特点:
针对老年代
采用标记-清除法清除垃圾;
基于"标记-清除"算法(不进行压缩操作,产生内存碎片);
以获取最短回收停顿时间为目标
优点:
并发收集、低停顿
垃圾收集线程与用户线程(基本上)可以同时工作
缺点:
1.对CPU资源非常敏感
因为并发标记和并发清除都是和程序同时运行,因此会占用CPU导致应用程序变慢
2.无法处理浮动垃圾
所以需要预留空间来分配产生的浮动垃圾。可能出现"Concurrent Mode Failure"失败
浮动垃圾就是在并发清除过程中新生成的垃圾,这部分垃圾CMS无法在本次被清理,可能出现Concurrent Mode Failed报错,因此需要预留一定的内存空间,无法等到老年代快被占满时再清除。默认情况下,CMS在老年代使用了92%后就会被激活。可以设置-XX:CMSInitiatingOccupancyFraction设置这个值。
如果真的出现了concurrent mode failed,说明已经没办法并发标记垃圾了,这时候就会使用serial old垃圾收集器来回收,也就是通过stop the world的方式。
3.产生大量内存碎片
往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次FullGC的情况
第一行参数:开启响应时间优先垃圾回收器
在老年代的垃圾回收器:
并发执行:垃圾回收线程和用户线程可以同时进行。
所以在垃圾回收的部分阶段是不需要stw的,所以减少了stw的时间
老年代空间不足的时候,执行老年代垃圾回收
有两种解释,一种是四个步骤,另一种是七个步骤
这里分析七个步骤的:
1.初始标记( 标记存活的对象,只是标记一些根对象) stw
2.并发标记
3.并发预处理
4.可终止的并发预处理
5.重新标记(Final Remark)(并发标记的时候用户线程也在工作,可能会对垃圾回收有干扰,所以重新标记) stw
6.并发清理
7.并发重置
其中初始标记和重新标记需要STW
1.标记老年代的GC Roots
2.标记年轻代中活着的对象引用到的老年代的对象
初始标记虽然STW,但是速度很快
初始标记阶段被标记为存活的对象作为起点,向下遍历,找出所有存活的对象。这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
同时,由于该阶段是用户线程和GC线程并发执行,对象之间的引用关系在不断发生变化,对于这些对象,都是需要进行重新标记的,否则就会出现错误
在这个阶段的执行过程中,可能会产生很多变化:
有些对象,从新生代晋升到了老年代;
有些对象,直接分配到了老年代;
老年代或者新生代的对象引用发生了变化
为了提升后续的效率,JVM 会利用写屏障(write barrier)将发生引用关系变化的对象所在的区域对应的 card 标记为 dirty,后续只需要扫描这些 dirty card 区域即可,避免扫描整个老年代。
它会
1.扫描所有标记为Direty的Card。标记被Dirty对象直接或间接引用的对象
2.清除Dirty对象的Card标识
前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,
如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty;
最后将6标记为存活,如下图所示:
这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作 ,从而减轻在Final Remark阶段的stop-the-world
在该阶段,主要循环的做两件事:
处理 From 和 To 区的对象,标记可达的老年代对象;
和上一个阶段一样,扫描处理Dirty Card中的对象。
具体执行多久,取决于许多因素,满足其中一个条件将会中止运行
执行循环次数达到了阈值;
执行时间达到了阈值;
新生代Eden区的内存使用率达到了阈值。
ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻代的引用,是为了下个阶段的重新标记阶段,扫描年轻代指向老年代的引用的时间减少;
主要目的是重新扫描之前所有并发处理阶段的所有残留更新对象。
预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。
需要有一个stop-the-world的阶段来完成最后的标记工作,这就是重新标记阶段(CMS标记阶段的最后一个阶段)
重新标记的内存范围是整个堆,包含_young_gen和_old_gen
为什么扫描新生代:
因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”
当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc
回收掉年轻代的无用的对象,并将对象放入幸存带或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间
清理删除掉标记阶段判断的已经死亡的对象由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
由于 CMS 并发清理阶段用户线程还在运行中,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS 无法在当次 GC 中处理掉它们,只好留待下一次 GC 时再清理掉。这一部分垃圾就称为**“浮动垃圾”**。
这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
CMS 的整个垃圾回收过程中只有2个阶段是 stop the world,一个是初始标记,一个是重新标记,初始标记只标记GC Roots直达的对象,因此一般不会耗时太久,而重新标记出现耗时久的现象则比较多见,通常如果CMS GC较慢,大多都是重新标记阶段较慢导致的。
Final Remark 阶段比较慢,比较常见的原因是在并发处理阶段引用关系变化很频繁,导致 dirty card 很多、年轻代对象很多。
比较常见的做法可以在 Final Remark 阶段前进行一次 YGC,这样年轻代的剩余待标记对象会下降很多,被视为GC Root 的对象数量骤减, Final Remark 的工作量就少了很多。
试想一下,在进行 YGC 时,如何判断是否存在老年代到新生代的引用?
一个简单的办法是扫描整个老年代,但是这个代价太大了,因此 JVM 引入了卡表来解决这个问题。
卡表又称为卡片标记(card marking)
其原理为,在逻辑上将老年代空间分割为若干个固定大小的连续区域,分割出来的每一个个区域就称为卡片(card)。另外,为每个卡片准备一个与其对应的标记位,最简单的实现方案是由字节数组实现,以卡的编号作为索引。每个卡的大小通常介于128~512字节之间,一般使用2的幂字节大小**,例如HotSpot使用512字节。**
当卡片内部发生引用变化时(指针写操作),写屏障会将该卡在卡表中对应的字节标记为脏(dirty)。
有了卡表后,在 YGC 时,只需将卡表中被标记为 dirty 的 card 也作为扫描范围,就可以保障不扫描整个老年代也不会有遗漏了。
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的