JVM CMS 源码分析

https://blog.csdn.net/weixin_45410925?t=1

为什么 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 分别来说明他们的触发条件
 

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)。
 

上述代码可知,从大类上分, 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

从源码上看,这里主要分成两类: (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 原因。
 

 CMS 何时会进行 Full GC

 CMS GC 分为 foreground collector 和 background collector。 不管是 foreground collector 还是 background collector 使用的都是 mark-sweep 算法,分阶段进行标记清理,优点很明显-低延时,但最大的缺点是存在碎片,内存空间利用率低。因此,CMS 为了解决这个问题,在每次进行 foreground collector 之前,判断是否需要进行一次压缩式 GC。

何时会进行 FullGC?

下面这段代码就是 CMS 进行判断是进行 mark-sweep 的 foreground collector,还是进行 mark-sweep-compact 的 Full GC。主要的判断依据就是是否进行压缩,即代码中的 should_compact。

接下来我们就来分析下在什么情况下会进行 compact, 来看 decideforegroundcollection_type 函数,主要分为 4 种情况:

GC(包含 foreground collector 和 compact 的 Full GC)次数

GCCause 是否是用户请求式触发导致的

增量 GC 是否可能会失败(悲观策略)

是否清理所有 SoftReference
 

1. GC(包含 foreground collector 和 compact 的 Full GC)次数
// UseCMSCompactAtFullCollection 参数值默认是 true    
UseCMSCompactAtFullCollection &&    
    ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction)
这里说的 GC 次数 fullgcssinceconc_gc,指的是从上次 background collector 后,foreground collector 和 compact 的 Full GC 的次数,只要次数大于等于 CMSFullGCsBeforeCompaction 参数阈值,就表示可以进行一次压缩式的 Full GC。 (CMSFullGCsBeforeCompaction 参数默认是 0,意味着默认是要进行压缩式的 Full GC)
2. GCCause 是否是用户请求式触发导致

用户请求式触发导致的 GCCause 指的是 javalangsystemgc(即 System.gc())或者 jvmtiforce_gc(即 JVMTI 方式的强制 GC)意味着只要是 System.gc(前提没有配置 ExplicitGCInvokesConcurrent 参数)调用或者 JVMTI 方式的强制 GC 都会进行一次压缩式的 Full GC。

3. 增量 GC 是否可能会失败(悲观策略)
 

JVM 源码解读之 CMS GC 触发条件 文章中也提到了这块内容, 指的是两代的 GC 体系中,主要指的是 Young GC 是否会失败。如果 Young GC 已经失败或者可能会失败,CMS 就认为可能存在碎片导致的,需要进行一次压缩式的 Full GC。

“incrementalcollectionfailed()” 这里指的是 Young GC 已经失败,至于为什么会失败一般是因为 Old Gen 没有足够的空间来容纳晋升的对象,比如常见的 “promotion failed” 。

“!getgen(0)->collectionattemptissafe()” 指的是 Young Gen 存活对象晋升是否可能会失败。 通过判断当前 Old Gen 剩余的空间大小是否足够容纳 Young GC 晋升的对象大小。 Young GC 到底要晋升多少是无法提前知道的,因此,这里通过统计平均每次 Young GC 晋升的大小和当前 Young GC 可能晋生的最大大小来进行比较。
 

4. 是否清理所有 SoftReference

  SoftReference 软引用,你应该了解它的特性,一般是在内存不够的时候,GC 会回收相关对象内存。这里说的就是需要回收所有软引用的情况,在配置了 CMSCompactWhenClearAllSoftRefs 参数的情况下,会进行一次压缩式的 Full GC。

JDK 1.9 有变更: 彻底去掉了 CMS forground collector 的功能,也就是说除了 background collector,就是压缩式的 Full GC。自然(UseCMSCompactAtFullCollection、CMSFullGCsBeforeCompaction 这两个参数也已经不在支持了。
 

总结
本文着重介绍了 CMS 在以下 4 种情况:

GC(包含 foreground collector 和 compact 的 Full GC)次数

GCCause 是否是用户请求式触发导致

增量 GC 是否可能会失败(悲观策略)

是否清理所有 SoftReference

会进行压缩式的 Full GC,并且详细介绍了每种情况下的触发条件。我们在 GC 调优时应该尽可能的避免压缩式的 Full GC,因为其使用的是 Serial Old GC 类似算法,它是单线程对全堆以及 metaspace 进行回收,STW 的时间会特别长,对业务系统的可用性影响比较大。
 

猜你喜欢

转载自blog.csdn.net/kuaipao19950507/article/details/106533380