前文已经讲过,CMS
是老年代垃圾收集器,在收集过程中可以与用户线程并发操作。它可以与Serial
收集器和Parallel New
收集器搭配使用。CMS
牺牲了系统的吞吐量来追求收集速度,适合追求垃圾收集速度的服务器上。
CMS相关参数
参数 | 类型 | 默认值 | 作用 |
---|---|---|---|
-XX:+UseConcMarkSweepGC | boolean | false | 老年代采用CMS收集器收集 |
–XX:ParallelGCThreads=n | int | (ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8) | 老年代采用CMS收集器收集 |
-XX:CMSInitiatingOccupancyFraction | int | 92 | 年代堆空间的使用率。比如value=75意味着第一次CMS垃圾收集会在老年代被占用75%时被触发。 |
-XX:+UseCMSInitiatingOccupancyOnly | boolean | false | 只用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整 |
触发条件
周期性GC
由后台线程ConcurrentMarkSweepThread
循环判断(默认2s)是否需要触发。
如果没有设置-XX:+UseCMSInitiatingOccupancyOnly
,虚拟机会根据收集的数据决定是否触发
老年代使用率达到阈值 CMSInitiatingOccupancyFraction
,默认92%。
永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction
,默认92%,前提是开启 CMSClassUnloadingEnabled
。
新生代的晋升担保失败。
主动触发
YGC
过程发生Promotion Failed
,进而对老年代进行回收
比如执行了System.gc()
,前提是没有参数ExplicitGCInvokesConcurrent
收集过程
初始标记
这是CMS
中两次stop-the-world
事件中的一次。这一步的作用是标记存活的对象,有两部分:
标记老年代中所有的GC Roots
对象
标记年轻代中活着的对象引用到的老年代的对象
CMS-initial-mark:961330K(1572864K)
指标记时老年代的已用空间和总空间
并发标记
该阶段GC
线程和应用线程并发执行,遍历InitialMarking
阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。
1 | [CMS-concurrent-mark-start] |
第一行CMS-concurrent-mark-start
标识标记阶段开始。第二行中的“2.787/3.329 secs”表示标记阶段的耗时。
表示花费了2.787cpu时间,3.329系统时间。
预清理阶段
由于在并发标记阶段,应用线程和GC
线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:
新生代的对象晋升到老年代;
直接在老年代分配对象;
老年代对象的引用关系发生变更;
该阶段会把上述对象所在的Card
标识为Dirty
,后续只需扫描这些Dirty Card
的对象,避免扫描整个老年代。
标记dirty card
能够到达的对象
1 | [CMS-concurrent-preclean-start] |
可终止的预处理
该阶段发生的前提是,新生代Eden
区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold
默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。
在该阶段,主要循环的做两件事:
处理 From
和 To
区的对象,标记可达的老年代对象
和上一个阶段一样,扫描处理Dirty Card
中的对象
重新标记
暂停所有用户线程,重新扫描堆中的对象,进行可达性分析,标记活着的对象。有了前面的基础,这个阶段的工作量被大大减轻,停顿时间因此也会减少。注意这个阶段是多线程的。
遍历新生代对象,重新标记
根据GC Roots
,重新标记
遍历老年代的Dirty Card
,重新标记,这里的Dirty Card
大部分已经在clean
阶段处理过
并发清理
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector
采用清扫的方式回收那些不能用的对象了。
这个阶段主要是清除那些没有标记的对象并且回收空间;
由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。
注意事项
减少remark阶段停顿
一般
CMS
的GC
耗时80%都在remark阶段,如果发现remark
阶段停顿时间很长,可以尝试添加该参数:-XX:+CMSScavengeBeforeRemark
。在执行remark
操作之前先做一次Young GC
,目的在于减少年轻代对老年代的无效引用,降低remark
时的开销。内存碎片问题
CMS
是基于标记-清除算法的,CMS
只会删除无用对象,不会对内存做压缩,会造成内存碎片,这时候我们需要用到这个参数:-XX:CMSFullGCsBeforeCompaction=n
意思是说在上一次CMS
并发GC
执行过后,到底还要再执行多少次full GC
才会做压缩。默认是0Concurrent mode failure
这个异常发生在cms正在回收的时候。执行
CMS GC
的过程中,同时业务线程也在运行,当年轻带空间满了,执行YGC
时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS
还没有机会回收老年带产生的,或者在做Minor GC
的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。
过早提升与提升失败
在Minor GC
过程中,Survivor Unused
可能不足以容纳Eden
和另一个Survivor
中的存活对象, 那么多余的将被移到老年代, 称为过早提升(Premature Promotion
),这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 再进一步,如果老年代满了,Minor GC
后会进行Full GC
, 这将导致遍历整个堆, 称为提升失败(Promotion Failure
)。早提升的原因
Survivor
空间太小,容纳不下全部的运行时短生命周期的对象,如果是这个原因,可以尝试将Survivor
调大,否则端生命周期的对象提升过快,导致老年代很快就被占满,从而引起频繁的full gc
;
对象太大,Survivor
和Eden
没有足够大的空间来存放这些大对象。提升失败原因
当提升的时候,发现老年代也没有足够的连续空间来容纳该对象。为什么是没有足够的连续空间而不是空闲空间呢?老年代容纳不下提升的对象有两种情况:- 老年代空闲空间不够用了;
- 老年代虽然空闲空间很多,但是碎片太多,没有连续的空闲空间存放该对象。
解决方法
如果是因为内存碎片导致的大对象提升失败,cms需要进行空间整理压缩;
如果是因为提升过快导致的,说明Survivor 空闲空间不足,那么可以尝试调大 Survivor;
如果是因为老年代空间不够导致的,尝试将CMS触发的阈值调低。
REF:https://juejin.im/post/5c39920b6fb9a049e82bbf94
参考地址
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。