CMS 垃圾回收器简介
由于上一篇博文已经详细介绍了,这里就不在赘述,有兴趣可以围观一下Java垃圾回收详解(2)
GC方式介绍
这里做一个简单的回顾:
-
获取最短回收停顿时间为目标的多线程并发收集器
-
cms 只会回收老年代和永久带(1.8开始为元数据区,需要设置-
CMSClassUnloadingEnabled
, 不会收集年轻带; -
cms 是一种预处理垃圾回收器,它不能等到old内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败;所以cms垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久带达到92%;
CMS 过程详解
CMS收集器的7个阶段:
-
初始标记(CMS initial mark)
-
并发标记(CMS concurrent mark)
-
并发预清理(CMS concurrent-preclean)
-
并发可取消的预清理 (CMS concurrent-abortable-preclean)
-
重新标记(CMS remark)
-
并发清除(CMS concurrent sweep)
-
并发重置(CMS concurrent-reset)
-
流程大致如下
- 具体过程如下
CMS 阶段详解
初始标记(Idling)阶段
这是CMS中两次stop-the-world事件中的一次。这一步的作用是标记存活的对象,有两部分:
- 标记老年代中所有的
GC Roots
对象,如下图节点1; - 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻带中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3;
在Java语言里,可作为GC Roots对象的包括如下几种:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;
- 方法区中的类静态属性引用的对象 ;
- 方法区中的常量引用的对象 ;
- 本地方法栈中JNI的引用的对象;
为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled
,同时调大并行标记的线程数,线程数不要超过cpu的核数
并发标记 (InitialMarking)阶段
从“初始标记”阶段标记的对象开始找出所有存活的对象,也就是从GC_ROOTS
触发,标记所有存活对象,由于第一次的结果,所以这次的标记并不需要STW。
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的。
否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
如下图所示,也就是节点1、2、3,最终找到了节点4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
最后将6标记为存活,如下图所示:
- Card Table 是什么
HotSpot 的一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。在进行 Minor GC 的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到 Minor GC 的 GC Roots 里。当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。
预清理 (Precleaning) 阶段
通过参数 CMSPrecleaningEnabled
选择关闭该阶段,默认启用,主要做两件事情:
-
处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
-
在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty,然后扫描这些dirty card
可中断的预清理(AbortablePreclean)阶段
该阶段发生是有前提的:
-
CMS 由于是在老年代垃圾扫描,但是大部分的老年代对象都是被
GC_ROOTS
引用的,如上图的 current obj 这个对象,虽然在老年代,但是其引用还是在年轻带,并且在并发标记阶段,也有可能老年代的某些对象别重新被新生代的对象重新引用。所以在gc的时候不仅需要扫描老年代也需要扫描新生代确定GC_ROOTS
。 -
全量的扫描新生代和老年代会不会很慢?肯定会,CMS 号称是停顿时间最短的GC,如此长的停顿时间肯定是不能接受的。 生代Eden区的内存使用量大于参数
CMSScheduleRemarkEdenSizeThreshold
默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。 -
如果新生代很多,超过了上述的阈值,如果在扫描新生代前进行一次Minor GC,那么在下一阶段remard的时候,新生代的对象会很少,从而节省很多时间。
-
CMS 有两个参数:
1 | CMSScheduleRemarkEdenSizeThreshold |
默认值分别是2M、50%。两个参数组合起来的意思是预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean
),直到eden空间使用率达到50%时中断,进入remark阶段。
-
如果能在可中止的预清理阶段发生一次 Minor GC,那就万事大吉、天下太平了。 这里有一个小问题,可终止的预清理要执行多长时间来保证发生一次Minor GC?答案是没法保证。道理很简单,因为垃圾回收是JVM自动调度的,什么时候进行GC我们控制不了。但此阶段总有一个执行时间吧?是的。
-
CMS提供了一个参数
CMSMaxAbortablePrecleanTime
,默认为5S。只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration
都会中止此阶段,进入remark。如果在5S内还是没有执行Minor GC怎么办?CMS提供CMSScavengeBeforeRemark
参数,使remark前强制进行一次Minor GC。
这样做利弊都有。好的一面是减少了remark阶段的停顿时间;坏的一面是Minor GC后紧跟着一个remark pause。如此一来,停顿时间也比较久。
我们来结合日志来看一下这个阶段
CMS日志如下:
1 | 7688.150: [CMS-concurrent-preclean-start] |
7688.186启动了可终止的预清理,在随后的三秒内启动了Minor GC,然后进入了Remark阶段.实际上为了减少remark阶段的STW时间
重新标记(FinalMarking)
这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark
,在重新标记之前,先执行一次ygc,回收掉年轻带的对象无用的对象,并将对象放入幸存带或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间。
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻带的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。
另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled
并发清理 ( CMS-concurrent-sweep )
这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。注意这个阶段会产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾。
并发重置 ( CMS-concurrent-reset )
这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。
CMS 调优
CMS JVM 参数
参数名 | 作用 |
---|---|
-XX:+PrintCommandLineFlags |
打印出启动参数行 |
-XX:+UseConcMarkSweepGC |
参数指定使用CMS垃圾回收器 |
-XX:+UseCMSInitiatingOccupancyOnly |
命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期。而是,当该标志被开启时,JVM通过 CMSInitiatingOccupancyFraction 的值进行每一次CMS收集,而不仅仅是第一次。然而,请记住大多数情况下,JVM比我们自己能作出更好的垃圾收集决策。因此,只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。 |
-XX:CMSInitiatingOccupancyFraction =80 |
参数指定CMS垃圾回收器在老年代达到80%的时候开始工作,如果不指定那么默认的值为92% |
-XX:+CMSClassUnloadingEnabled |
开启永久带(jdk1.8以下版本)或元数据区(jdk1.8及其以上版本)收集,如果没有设置这个标志,一旦永久代或元数据区耗尽空间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC |
-XX:+UseParNewGC |
使用cms时默认这个参数就是打开的,不需要配置,cms只回收老年代,年轻带只能配合Parallel New或Serial回收器; |
-XX:+CMSParallelRemarkEnabled |
减少Remark阶段暂停的时间,启用并行Remark,如果Remark阶段暂停时间长,可以启用这个参数 用于重新标记阶段是否采用多线程并行执行 |
-XX:+CMSScavengeBeforeRemark | 若Remark阶段暂停时间太长,可以启用这个参数,在Remark执行之前,先做一次ygc。因为这个阶段,年轻带也是cms的gcroot,cms会扫描年轻带指向老年代对象的引用,如果年轻带有大量引用需要被扫描,会让Remark阶段耗时增加 |
-XX:CMSFullGCsBeforeCompaction =0 -XX:+ UseCMSCompactAtFullCollection |
两个参数是针对cms垃圾回收器碎片做优化的,CMS是不会移动内存的, 运行时间长了,会产生很多内存碎片, 导致没有一段连续区域可以存放大对象,出现promotion failed 、concurrent mode failure , 导致fullgc,启用UseCMSCompactAtFullCollection 在FULL GC的时候, 对年老代的内存进行压缩。-XX:CMSFullGCsBeforeCompaction=0 则是代表多少次FGC后对老年代做压缩操作,默认值为0,代表每次都压缩, 把对象移动到内存的最左边,可能会影响性能,但是可以消除碎片,这两个错误会在后面的章节中详细介绍 |
-XX:+CMSConcurrentMTEnabled -XX: ConcGCThreads =4 |
定义并发CMS过程运行时的线程数。比如value=4意味着CMS周期的所有阶段都以4个线程来执行。尽管更多的线程会加快并发CMS过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加CMS线程数是否真的能够带来性能的提升。如果未设置这个参数,JVM会根据并行收集器中的-XX:ParallelGCThreads 参数的值来计算出默认的并行CMS线程数,此字段如果不清楚的情况下不要设置ParallelGCThreads = (ncpus <=8 ? ncpus : 8+(ncpus-8)*5/8) (ncpus为cpu个数)ConcGCThreads =(ParallelGCThreads + 3)/4 |
-XX:+ExplicitGCInvokesConcurrent -XX:+ ExplicitGCInvokesConcurrentAndUnloadsClasses |
开启foreground CMS GC,CMS gc 有两种模式,background和foreground,正常的cms gc使用background模式,就是我们平时说的cms gc;当并发收集失败或者调用了System.gc()的时候,就会导致一次full gc,这个fullgc是不是cms回收,而是Serial单线程回收器,加入了参数-XX:+ExplicitGCInvokesConcurrent 后,执行full gc的时候,就变成了CMS foreground gc,它是并行full gc,只会执行cms中stop the world阶段的操作,效率比单线程Serial full GC要高;需要注意的是它只会回收old,因为cms收集器是老年代收集器;而正常的Serial收集是包含整个堆的,加入了参数-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses ,代表永久带也会被cms收集; |
-XX:+CMSParallelInitialMarkEnabled | 初始标记阶段是否采用多线程并行执行 |
- -XX:+PrintGCDetails | 日志详细打印参数 |
- -XX:+PrintGCCause | 日志详细打印参数 |
- -XX:+PrintGCTimeStamps | 日志详细打印参数 |
- -XX:+PrintGCDateStamps | 日志详细打印参数 |
- -Xloggc:…/logs/gc.log | 日志详细打印参数 |
日志分析
1 | 2017-07-27T10:42:56.291-0800: 6.963: [GC (CMS Final Remark) |
CMS gc 分析
1 | /第一步 初始标记 这一步会停顿 |
CMS 的处理流程讲完了,由于篇幅有些长,分一下章节,下次会围绕CMS 的后续问题 展开一些讨论 欢迎阅读。
参考:
垃圾回收统一理论
Introduction to Garbage Collection
R大:并发垃圾收集器(CMS)为什么没有采用标记-整理算法来实现?
R大:请教Weak Reference及其在HotSpot GC中的行为?
R大:请教Weak Reference及其在HotSpot GC中的行为?
Java垃圾回收浅析(2)-GC方式介绍
赞赏一下