CMS全称
Concurrent Mark Sweep
,是一款并发的、使用标记-清除算法的垃圾回收器,
如果老年代使用CMS垃圾回收器,需要添加虚拟机参数-"XX:+UseConcMarkSweepGC"。
CMS的另一个缺点是它需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已 避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值。
周期性Old GC,执行的逻辑也叫
Background Collect
,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。触发条件
1、如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(建议线上环境带上这个参数,不然会加大问题排查的难度)。
2、老年代使用率达到阈值CMSInitiatingOccupancyFraction
,默认92%。
3、永久代的使用率达到阈值CMSInitiatingPermOccupancyFraction
,默认92%,前提是开启CMSClassUnloadingEnabled
。
4、新生代的晋升担保失败。CMS GC要决定是否在full GC时做压缩,会依赖几个条件。其中:
- 第一种条件,UseCMSCompactAtFullCollection 与 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默认就是true了,也就是关键在后者上。
- 第二种条件是用户调用了System.gc(),而且DisableExplicitGC没有开启。
- 第三种条件是young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。
- 上述三种条件的任意一种成立都会让CMS决定这次做full GC时要做压缩。
CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前VM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。 本来这个参数就是用来配置降低full GC压缩的频率,以期减少某些full GC的暂停时间。CMS回退到full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些但这次full GC的暂停时间会短些;这是个取舍。
2. -XX:CMSInitiatingOccupancyFraction=70 和-XX:+UseCMSInitiatingOccupancyOnly
这两个设置一般配合使用,一般用于『降低CMS GC频率或者增加频率、减少GC时长』的需求
-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC);
-XX:+UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.
3. -XX:+CMSScavengeBeforeRemark
在CMS GC前启动一次ygc,目的在于减少old gen对ygc gen的引用,降低remark时的开销-----一般CMS的GC耗时 80%都在remark阶段
-XX:+UseCMSInitiatingOccupancyOnly
我们用-XX+UseCMSInitiatingOccupancyOnly标志来命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期。而是,当该标志被开启时,JVM通过CMSInitiatingOccupancyFraction的值进行每一次CMS收集,而不仅仅是第一次。然而,请记住大多数情况下,JVM比我们自己能作出更好的垃圾收集决策。因此,只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。
1:使用cms gc必备的三个参数
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=n
-XX:+UseCMSInitiatingOccupancyOnly
默认的NewRatio未生效,新生代的大小不确定
默认的NewRatio为2,表示新生代和老年代比例是1:2,即占堆的1/3
但是实际设置了-Xmx和-Xms后,新生代的大小不符合预期
原因:runtime.arguments.cpp
else if (UseConcMarkSweepGC) {
set_cms_and_parnew_gc_flags();
}const size_t preferred_max_new_size_unaligned =
MIN2(max_heap/(NewRatio+1), ScaleForWordSize(young_gen_per_worker * parallel_gc_threads));
即cms新生代的大小是计算出来的
所以通常使用cms的时候,建议手动指定新生代大小参数(-XX:NewRatio或者-Xmn或者-XX:NewSize/-XX:MaxNewSize)
另外JDK-6862534 : -XX:NewRatio completely ignored when combined with -XX:+UseConcMarkSweepGC,之前是即使手动指定-XX:NewRatio,也无效,现早已修复
使用jstat -gccause pid观察cms fgc的时候,发现每次到阈值回收的时候,fgc每次会跳2次
如果观察cms fgc,突然发现stw的时间很长,多达几秒甚至更多,一定是出现了异常情况,而这些情况的代价都十分昂贵,在做cms调优的时候要尽可能的避免
错误:并发模式失败(Concurrent mode failure)
1. 在cms并发周期执行期间,用户的线程依然在运行,如果这时候如果应用线程向老年代请求分配的空间超过预留的空间,就会抛出该错误 - 后台线程的收集没有赶上应用线程的分配速度
2. 有时候“空间不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足,而浮动垃圾就是cms执行期间用户线程申请的内存空间
3. 这个错误可能触发两种情况
> cms的foreground模式(默认的cms gc属于background模式),这个模式是CMS自己的mark-sweep来做不并发的(串行的)old generation GC,不过会将一些阶段省略掉。
+ CMS的foreground collector的算法就是普通的mark-sweep。它收集的范围只是CMS的old generation,而不包括其它generation。因而它在HotSpot VM里不叫做full GC
> Serial Old GC
+ mark-sweep-compact算法
+ 它收集的范围是整个GC堆,包括Java heap的young generation和old generation,以及non-Java heap的permanent generation。因而其名 Full GC
> 前者的出现原因:A STW foreground collection can pick up where a concurrent background collection left off to try to avoid a full GC. This is nice but normally it has worse performance than a full GC.
+ 即是为了避免fgc,但是往往性能甚至比fgc更差
> 对于第一种foreground模式,必须要 -XX:-UseCMSCompactAtFullCollection & -XX:CMSFullGCsBeforeCompaction设置大于0
+ 但是UseCMSCompactAtFullCollection默认为true,CMSFullGCsBeforeCompaction默认是0,所以一定会触发第二种Serial Old GC
> 参考:
+ https://bugs.openjdk.java.net/browse/JDK-8010202
+ https://bugs.openjdk.java.net/browse/JDK-8064702
+ https://bugs.openjdk.java.net/browse/JDK-8027132
+ 均建议foreground collector在Java8废弃,在Java9移除,包括UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction这两个参数
4. 所以通常来说不建议设置上面两个参数,否则可能在Java8中会触发foreground collector,可能会更慢(单线程)。所以通常当出现concurrent mode failure时触发的都是Serial Old GC
1. 关于UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction的警告源代码
runtime\arguments.cpp
if (FLAG_IS_CMDLINE(UseCMSCompactAtFullCollection)) {
warning("UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release.");
}
if (FLAG_IS_CMDLINE(CMSFullGCsBeforeCompaction)) {
warning("CMSFullGCsBeforeCompaction is deprecated and will likely be removed in a future release.");
}2. 关于用哪种处理方式的源代码 gc_implementation/concurrentMarkSweep/concurrentMarkSweepGeneration.cpp
void CMSCollector::acquire_control_and_collect{
...
bool should_compact = false;
decide_foreground_collection_type(clear_all_soft_refs,
&should_compact, &should_start_over);
...if (should_compact) {
...
// 这个就是mark-sweep-compact 的 Full GC
do_compaction_work(clear_all_soft_refs);
...}else {
// mark-sweep
do_mark_sweep_work(clear_all_soft_refs, first_state,
should_start_over);
}*should_compact =
UseCMSCompactAtFullCollection &&
((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||
GCCause::is_user_requested_gc(gch->gc_cause()) ||
gch->incremental_collection_will_fail(true /* consult_young */));
而should_compact主要的一个判断逻辑就是判断UseCMSCompactAtFullCollection和CMSFullGCsBeforeCompaction这两个参数
错误:promotion failed
1. Java Performance,The Definitive Guide的原文是这样描述的:
- Here, CMS started a young collection and assumed that there was enough free space to hold all the promoted objects (otherwise, it would have declared a concurrent mode failure). That assumption proved incorrect: CMS couldn’t promote the objects because the old generation was fragmented (or, much less likely, because the amount of memory to be promoted was bigger than CMS expected).
- 翻译:新生代垃圾收集,判断老年代似乎有足够的空闲空间可以容纳所有的晋升对象(否则,CMS收集器会报concurrent mode failure)。这个假设最终被证明是错误的,由于老年代空间的碎片化(或者,不太贴切的说,由于晋升实际要占用的内存超过了CMS收集器的判断),CMS收集器无法晋升这些对象。
2. Sometimes we see these promotion failures even when thelogs show that there is enough free space in tenured generation. The reason is'fragmentation' - the free space available in tenured generation is notcontiguous, and promotions from young generation require a contiguous freeblock to be available in tenured generation. CMS collector is a non-compactingcollector, so can cause fragmentation of space for some type of applications.
- 翻译:CMS收集器对老年代收集的时候,不再进行任何压缩和整理的工作,意味着老年代随着应用的运行会变得碎片化;碎片过多会影响大对象的分配,虽然老年代还有很大的剩余空间,但是没有连续的空间来分配大对象
3. 如果在ParNew准备收集时CMS说晋升没问题,但ParNew已经开始收集之后确实遇到了晋升失败的情况
4. promotion failed是说,担保机制确定老年代是否有足够的空间容纳新来的对象,如果担保机制说有,但是真正分配的时候发现由于碎片导致找不到连续的空间而失败;而concurrent mode failure是指并发周期还没执行完,用户线程就来请求比预留空间更大的空间了,即后台线程的收集没有赶上应用线程的分配速度。
5. promotion failed触发fgc,触发模式同上,通常也是Serial Old GC
错误:permgen (or the metaspace) fills up
1. 对于Java8来说,这个主要是在metaspace扩容时触发的
2. 如果老年代设置了 CMS,则 Metasapce 扩容引起的 FGC 会转变成一次 CMS
3. Java8中收集器默认就会收集元空间中不再载入的类
在刚启动应用后,通过jstat -gccause pid后看到出现了fgc,此时ou也没有占用
jstat -gccause 23270 1000
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC
0.00 25.87 82.46 0.00 97.47 94.80 1 0.124 2 0.096 0.220 Metadata GC Threshold No GC
通过观察gc日志,出现cms异常的几种情况
[ParNew (promotion failed): ... (concurrent mode failure):...
这种情况是先出现了promotion failed,然后准备触发fgc
而此时cms这在执行并发收集,此时则执行打断逻辑,输出concurrent mode failure
具体源代码也是concurrentMarkSweepGeneration.cpp
if (first_state > Idling) {
report_concurrent_mode_interruption();
}
[ParNew (promotion failed): ...
- 这种情况就是单纯出现了promotion failed,此时cms未执行并发收集
(concurrent mode failure): ...
- 这种情况是单纯的cms正在执行并发收集,然后用户线程申请内存空间不足
jvm有一个内存担保机制,是类似于判断'老年代最大的可用连续空间是否大于新生代所有对象的总和'。但通常描述promotion failed的时候是指担保机制够了, 才会发生。那么既然有最大可用连续空间,为什么还会failed
原则
针对concurrent mode failure的优化
发生该失败的主要原因是由于CMS不能以足够快的速度清理老年代空间
当老年代空间的占用达到某个阈值时,并发回收就开始了。一个CMS后台线程开始扫描老年代空间,寻找无用的垃圾对象时,竞争就开始了。CMS收集器必须在老年代剩余的空间用尽之前,完成老年代空间的扫描及回收工作。否则如果在正常速度的比赛中失效,就会发生该错误
在并发清理阶段,用户线程仍然在运行,必须预留出空间给用户线程使用,会产生’浮动垃圾‘
常规优化途径如下:
以更高的频率执行后台的回收线程,即提高CMS并发周期发生的频率
主要是调低CMSInitiatingOccupancyFraction的值
但是不能太低,太低会导致过于频繁的gc,会消耗更多的的cpu和停顿
landon
需要先计算老年代常驻内存大小,如占用60%,那么这个阈值则可以设置为约70%,否则会比较频繁gc
可以考虑担保机制,只要老年代预留剩余空间大于年轻代大小,比如新生代和老年代的比例是1 : 4,即新生代占用老年代的25%,那么这个阈值可以设置为70,即老年代还预留出来30%的空间
注意如果浮动垃圾很多的话,也无法解决该问题,即cms并发回收期间,浮动垃圾越来越多,占用预留空间,多次的ygc的话,会有填满预留空间的可能,虽然概率较低
两个条件综合考虑,如果设置了阈值70,但是老年代常驻内存很大,甚至超过70,那么此时的建议要提高堆内存,增加老年代的大小或者减少新生代的大小
针对promotion failed的优化
这个是cms最为严重的’碎片问题‘,我们要尽量避免这个发生后引起的fgc
所以优化这个问题,也可以描述为'如何解决碎片问题'
常规优化途径如下
增大堆内存,增加老年代大小,但要注意不要超过32g(the HotSpot JVM uses a trick to compress object pointers when heaps are less than around 32 GB)
尽早执行cms gc,合理设置CMSInitiatingOccupancyFraction,会合并老生代中相邻的free空间,可分配给较大的对象
和上面一样,也可以做一个老年代预留空间大于年轻代
到了阈值后,就会触发cms gc,但还是和上面说的,会产生浮动垃圾 + 碎片,还是会出现
另外一个比较“挫”的办法,是在每天凌晨访问量低的时候,主动执行一下fgc,执行一下'碎片压缩'
如System.gc,但是要注意是否开启了-XX:+ExplicitGCInvokesConcurrent
所以建议办法是用jmap -histo:live
另外晋升还包括to space空间小,可以根据情况尝试提高Survivor
1:日志,主要是用来排查cms相关问题
基础参数:
-Xloggc:gc_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
可选调试参数:
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution
-XX:+PrintPromotionFailure
-XX:+PrintHeapAtGC
-XX:PrintFLSStatistics=1
2:cms相关
1. 物理机内存:16G
2. 预估老年代常驻对象如Player 3000,一个Player平均2M,大约6G,所以老年代比如建议10G
3. -Xms12G -Xmx12G
4. 设置新生代2G,老年代10G
5. 设置CMSInitiatingOccupancyFraction为70,则老年代剩余空间为3G,大于新生代大小
6. 可选:-XX:+CMSScavengeBeforeRemark
简单算法:
-XX:NewRatio=4,即新生代和老年代1:4
然后设置CMSInitiatingOccupancyFraction为70,即老年代剩余空间稍大新生代
但要保证这个70基本上要大于老年代常驻内存,否则可能会频繁cms gc
另外建议增加脚本,尝试手动执行fgc,整理碎片
如每天凌晨3点
jstat -gccause pid >> cms.log
jmap -histo pid >> cms.log
jstat -gccause pid >> cms.log
jmap -histo:live pid >> cms.log
3:metaspace
设置 -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=512m
注意如果设置的过小,则会引起fgc甚至metaspace oom