JVM 源码解读之 CMS GC 触发条件
前言
经常有同学会问,为啥我的应用 Old Gen 的使用占比没达到 CMSInitiatingOccupancyFraction 参数配置的阈值,就触发了 CMS GC,表示很莫名奇妙,不知道问题出在哪?
其实 CMS GC 的触发条件非常多,不只是 CMSInitiatingOccupancyFraction 阈值触发这么简单。本文通过源码全面梳理了触发 CMS GC 的条件,尽可能的帮你了解平时遇到的奇奇怪怪的 CMS GC 问题。
先抛出一些问题,来吸引你的注意力。
为什么 Old Gen 使用占比仅 50% 就进行了一次 CMS GC?
Metaspace 的使用也会触发 CMS GC 吗?
为什么 Old Gen 使用占比非常小就进行了一次 CMS GC?
触发条件
CMS GC 在实现上分成 foreground collector 和 background collector。foreground collector 相对比较简单,background collector 比较复杂,情况比较多。
下面我们从 foreground collector 和 background collector 分别来说明他们的触发条件:
说明:本文内容是基于 JDK 8
说明:本文仅涉及 CMS GC 的触发条件,至于算法的具体过程,以及什么时候进行 MSC(mark sweep compact)不在本文范围
foreground collector
foreground collector 触发条件比较简单,一般是遇到对象分配但空间不够,就会直接触发 GC,来立即进行空间回收。采用的算法是 mark sweep,不压缩。
background collector
说明 background collector 的触发条件之前,先来说下 background collector 的流程,它是通过 CMS 后台线程不断的去扫描,过程中主要是判断是否符合 background collector 的触发条件,一旦有符合的情况,就会进行一次 background 的 collect。
每次扫描过程中,先等 CMSWaitDuration 时间,然后再去进行一次 shouldConcurrentCollect 判断,看是否满足 CMS background collector 的触发条件。CMSWaitDuration 默认时间是 2s(经常会有业务遇到频繁的 CMS GC,注意看每次 CMS GC 之间的时间间隔,如果是 2s,那基本就可以断定是 CMS 的 background collector)。
那 shouldConcurrentCollect() 方法中都有哪些条件呢?
上述代码可知,从大类上分, background collector 一共有** 5 种触发情况**:
1.是否是并行 Full GC
指的是在GC cause 是 gclocker 且配置了 GCLockerInvokesConcurrent 参数, 或者 GC cause 是javalangsystemgc(就是 System.gc()调用)and 且配置了 ExplicitGCInvokesConcurrent 参数,这时会触发一次 background collector。
2.根据统计数据动态计算(仅未配置 UseCMSInitiatingOccupancyOnly 时)
未配置 UseCMSInitiatingOccupancyOnly 时,会根据统计数据动态判断是否需要进行一次 CMS GC。
判断逻辑是,如果预测 CMS GC 完成所需要的时间大于预计的老年代将要填满的时间,则进行 GC。
这些判断是需要基于历史的 CMS GC 统计指标,然而,第一次 CMS GC 时,统计数据还没有形成,是无效的,这时会跟据 Old Gen 的使用占比来判断是否要进行 GC。
那占多少比率,开始回收呢?(也就是 bootstrapoccupancy 的值是多少呢?)
答案是 50%。或许你已经遇到过类似案例,在没有配置 UseCMSInitiatingOccupancyOnly 时,发现老年代占比到 50% 就进行了一次 CMS GC,当时的你或许还一头雾水呢。
3.根据 Old Gen 情况判断
从源码上看,这里主要分成两类:
(a) Old Gen 空间使用占比情况与阈值比较,如果大于阈值则进行 CMS GC
也就是"occupancy() > initiatingoccupancy()",occupancy 毫无疑问是 Old Gen 当前空间的使用占比,而 initiatingoccupancy 是多少呢?
可以看到当 CMSInitiatingOccupancyFraction 参数配置值大于 0,就是 “io / 100.0”;
当 CMSInitiatingOccupancyFraction 参数配置值小于 0 时(注意,默认是 -1),是 “((100 - MinHeapFreeRatio) + (double)(tr * MinHeapFreeRatio) / 100.0) / 100.0”,这到底是多少呢?是 92%,这里就不贴出具体的计算过程了,或许你已经在某些书或者博客中了解过,CMSInitiatingOccupancyFraction 没有配置,就是** 92,但是其实 CMSInitiatingOccupancyFraction 没有配置是 -1,所以阈值取后者 92%,并不是 CMSInitiatingOccupancyFraction 的值是 92。**
(b) 接下来没有配置 UseCMSInitiatingOccupancyOnly 的情况
这里也分成有两小类情况:
当 Old Gen 刚因为对象分配空间而进行扩容,且成功分配空间,这时会考虑进行一次 CMS GC;
根据 CMS Gen 空闲链判断,这里有点复杂,目前也没整清楚,好在按照默认配置其实这里返回的是 false,所以默认是不用考虑这种触发条件了。
4.根据增量 GC 是否可能会失败(悲观策略)
什么意思呢?两代的 GC 体系中,主要指的是 Young GC 是否会失败。如果 Young GC 已经失败或者可能会失败,JVM 就认为需要进行一次 CMS GC。
我们看两个判断条件,“incrementalcollectionfailed()” 和 “!getgen(0)->collectionattemptissafe()”
incrementalcollectionfailed() 这里指的是 Young GC 已经失败,至于为什么会失败一般是因为 Old Gen 没有足够的空间来容纳晋升的对象。
!getgen(0)->collectionattemptissafe() 指的是新生代晋升是否安全。
通过判断当前 Old Gen 剩余的空间大小是否足够容纳 Young GC 晋升的对象大小。
Young GC 到底要晋升多少是无法提前知道的,因此,这里通过统计平均每次 Young GC 晋升的大小和当前 Young GC 可能晋升的最大大小来进行比较。
5.根据 meta space 情况判断
这里主要看 metaspace 的 shouldconcurrent_collect 标志,这个标志在 meta space 进行扩容前如果配置了 CMSClassUnloadingEnabled 参数时,会进行设置。这种情况下就会进行一次 CMS GC。因此经常会有应用启动不久,Old Gen 空间占比还很小的情况下,进行了一次 CMS GC,让你很莫名其妙,其实就是这个原因导致的。
总结
本文梳理了 CMS GC 的 foreground collector 和 background collector 的触发条件,foreground collector 的触发条件相对来说比较简单,而 background collector 的触发条件比较多,分成 5 大种情况,各大种情况种还有一些小的触发分支。尤其是在没有配置 UseCMSInitiatingOccupancyOnly 参数的情况下,会多出很多种触发可能,一般在生产环境是强烈建议配置 UseCMSInitiatingOccupancyOnly 参数,以便于能够比较确定的执行 CMS GC,另外,也方便排查 GC 原因。
看完三件事❤️
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
关注公众号 『 java烂猪皮 』,不定期分享原创知识。
同时可以期待后续文章ing🚀
作者:涤生
评论