CMS 垃圾收集流程
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CMS 垃圾收集流程相关的知识,希望对你有一定的参考价值。
参考技术A CMS 处理过程有七个步骤:在Java语言里,可作为GC Roots对象的包括如下几种:
从“初始标记”阶段标记的对象开始找出所有存活的对象;
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;
如下图所示,也就是节点1、2、3,最终找到了节点4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
由于这个阶段是和用户线程并发的,可能会导致 concurrent mode failure
1) promotion failed不会导致fullGC中的CMS退化为serialGC
因为promotion fail后,youngGC就被停止了,开始fullGC。
2)发生concurrent mode failure会引起Full GC,这种情况下会使用Serial Old收集器,是单线程的,对GC的影响很大。concurrent mode failure产生的原因是老年代剩余的空间不够,导致了和gc线程并发执行的用户线程创建的大对象(由PretenureSizeThreshold控制新生代直接晋升老年代的对象size阀值)不能进入到老年代,只要stop the world来暂停用户线程,执行GC清理,单线程对全堆以及 metaspace 进行回收,STW 的时间会特别长,对业务系统的可用性影响比较大。可以通过设置CMSInitiatingOccupancyFraction预留合适的CMS执行时剩余的空间
一般CMS的GC耗时80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:
-XX:+CMSScavengeBeforeRemark。
在执行remark操作之前先做一次Young GC,目的在于减少年轻代对老年代的无效引用,降低remark时的开销
参考:
CMS垃圾收集器
CMS全称?ConcurrentMarkSweep,是一款并发的、使用标记-清除算法的垃圾回收器, 如果老年代使用CMS垃圾回收器,需要添加虚拟机参数-“XX:+UseConcMarkSweepGC”
缺点:
- CMS收集器对CPU资源非常敏感,在并发阶段,它虽然不会导致用户线程停顿,但是由于占用了一部分线程,所以会导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(cpu数量+3)/4。
- CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致一次Full GC。在JDK1.6中,CMS收集器当老年代使用了92%的空间后才会进行收集,所以如果CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”。通过-XX:CMSInitiatingOccupancyFraction可以调整百分比。
该参数必须配合UseCMSInitiatingOccupancyOnly使用才有效 - CMS由于是基于标记-清除算法实现的收集器,所以就可能会在收集结束的时候产生大量空间碎片。如果老年代中没有足够大的连续空间来分配当前对象,那么就会可能提前触发Full GC并进行碎片整理
JVM参数
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:D:aa.gc
-XX:+PrintTenuringDistribution
-XX:+UseConcMarkSweepGC
-Xmx1024m
-Xms1024m
日志如下:
2019-04-11T16:30:48.164+0800: 14.035: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(699072K)] 33843K(1013632K), 0.0066881 secs] [Times: user=0.09 sys=0.00, real=0.01 secs]
2019-04-11T16:30:48.179+0800: 14.044: [CMS-concurrent-mark-start]
2019-04-11T16:30:48.179+0800: 14.044: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-11T16:30:48.179+0800: 14.044: [CMS-concurrent-preclean-start]
2019-04-11T16:30:48.179+0800: 14.047: [CMS-concurrent-preclean: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-04-11T16:30:48.179+0800: 14.047: [CMS-concurrent-abortable-preclean-start]
2019-04-11T16:30:52.428+0800: 18.302: [CMS-concurrent-abortable-preclean: 2.860/4.254 secs] [Times: user=9.47 sys=1.09, real=4.25 secs]
2019-04-11T16:30:52.428+0800: 18.302: [GC (CMS Final Remark) [YG occupancy: 203965 K (314560 K)]2019-04-11T16:30:52.428+0800: 18.303: [Rescan (parallel) , 0.0177423 secs]2019-04-11T16:30:52.447+0800: 18.320: [weak refs processing, 0.0001500 secs]2019-04-11T16:30:52.447+0800: 18.320: [class unloading, 0.0287879 secs]2019-04-11T16:30:52.475+0800: 18.349: [scrub symbol table, 0.0121125 secs]2019-04-11T16:30:52.497+0800: 18.362: [scrub string table, 0.0012159 secs][1 CMS-remark: 0K(699072K)] 203965K(1013632K), 0.0623200 secs] [Times: user=0.13 sys=0.00, real=0.07 secs]
Old GC过程
- 初始化标记(CMS Initial Mark)
2019-04-11T16:30:48.164+0800: 14.035: [GC (CMS Initial Mark) [1 CMS-initial-mark: 0K(699072K)] 33843K(1013632K), 0.0066881 secs] [Times: user=0.09 sys=0.00, real=0.01 secs]
整个过程会STW。
标记GC Roots可达的老年代对象;
遍历新生代对象,标记可达的老年代对象; - 并发标记(CMS-concurrent-mark)
2019-04-11T16:30:48.179+0800: 14.044: [CMS-concurrent-mark-start]
2019-04-11T16:30:48.179+0800: 14.044: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
该阶段GC线程和应用线程并发执行,遍历InitialMarking阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。
- 预处理(CMS-concurrent-preclean)
2019-04-11T16:30:48.179+0800: 14.044: [CMS-concurrent-preclean-start]
2019-04-11T16:30:48.179+0800: 14.047: [CMS-concurrent-preclean: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
- 可中断的预清理(CMS-concurrent-abortable)
2019-04-11T16:30:48.179+0800: 14.047: [CMS-concurrent-abortable-preclean-start]
2019-04-11T16:30:52.428+0800: 18.302: [CMS-concurrent-abortable-preclean: 2.860/4.254 secs] [Times: user=9.47 sys=1.09, real=4.25 secs]
该阶段发生的前提是,新生代Eden区的内存使用量大于参数?CMSScheduleRemarkEdenSizeThreshold?默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。
因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。
- 重新标记(CMS Final Remark)
2019-04-11T16:30:52.428+0800: 18.302: [GC (CMS Final Remark) [YG occupancy: 203965 K (314560 K)]2019-04-11T16:30:52.428+0800: 18.303: [Rescan (parallel) , 0.0177423 secs]2019-04-11T16:30:52.447+0800: 18.320: [weak refs processing, 0.0001500 secs]2019-04-11T16:30:52.447+0800: 18.320: [class unloading, 0.0287879 secs]2019-04-11T16:30:52.475+0800: 18.349: [scrub symbol table, 0.0121125 secs]2019-04-11T16:30:52.497+0800: 18.362: [scrub string table, 0.0012159 secs][1 CMS-remark: 0K(699072K)] 203965K(1013632K), 0.0623200 secs] [Times: user=0.13 sys=0.00, real=0.07 secs]
并发重新标记,STW过程。
遍历新生代对象,重新标记根据GC Roots,重新标记。
针对并发模式失效的调优
发生并发模式失效往往是由于CMS不能以足够快的速度清理老年代空间:新生代需要进行垃圾回收时,CMS收集器计算发现老年代没有足够的空闲空间可以容纳这些晋级对象,不得不先对老年代进行垃圾回收。
当老年代空间的占用达到某个程度时,并发回收就开始了。一个CMS后台线程要在老年代剩余空间用尽之前,完成老年代空间的扫描回收工作。如果并发回收速度太慢,就会发生并发模式失效。
可以通过以下途径避免这种失效:
- 想办法增大老年代空间,要么只移动部分的新生代对象到老年代,要么增加更多的堆空间。
- 以更高的频率运行后台回收线程。
- 使用更多的后台回收线程。
如果有更多的内存可用,更好的方案是增加堆的大小,否则可以尝试调整后台线程运行的方式来解决这个问题。
给后台线程更多的运行机会
同时设置-XX:CMSInitiatingOccupancyFraction=N和-XX:+UseCMSInitiatingOccupancyOnly。对特定的程序,该标志的更优值可以根据GC日志中CMS周期首次启动失败时的值得到。具体方法是,在垃圾回收日志中寻找并发模式失效,找到后再反向查找CMS周期最近的启动记录。日志中含有CMS-initial-mark信息的一行包含了CMS周期启动时,老年代空间的占用情况。
调整CMS后台线程
每个CMS后台线程都会100%地占用机器上的一颗CPU。如果应用程序发生并发模式失效,同时又有额外的CPU周期可用,可以设置-XX:ConcGCThreads=N标志,增加后台线程的数目。默认ConcGCThreads =(ParallelGCThreads+3)/4。当有4个并行线程时,有1个并发线程,
当有5~8个并行线程时,有2个并发线程。ParallelGCThreads表示的是GC并行时使用的线程数,如果新生代使用ParNew,那么ParallelGCThreads也就是新生代GC线程数。默认情况下,当CPU数量小于8时,ParallelGCThreads的值就是CPU的数量,当CPU数量大于8时,ParallelGCThreads的值等于3+5*cpuCount/8。
java学习笔记/jvm
以上是关于CMS 垃圾收集流程的主要内容,如果未能解决你的问题,请参考以下文章