回顾
继上一节,讨论了CMS的gc 流程,本篇讨论几个遗留的问题。
CMS 的缺点
CMS GC 和Full GC 和 Minor GC 的区别
-
CMS GC
通过一个后台线程触发,触发机制是默认每隔2秒判断一下当前老年代的内存使用率是否达到阈值,当然具体的触发条件没有这么简单,如果是则触发一次cms gc,在该过程中只会标记出存活对象,然后清除死亡对象,期间会产生碎片空间。
-
Full GC
是通过 vm thread 执行的,整个过程是 stop-the-world,在该过程中会判断当前 gc 是否需要进行compact,即把存活对象移动到内存的一端,可以有效的消除cms gc产生的碎片空间。
-
Minor GC
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:
当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。
对年轻代的 Eden 和 Survivor 区进行了标记和复制操作,无碎片。
CMS GC 如何触发
对于 cms gc 来说,触发条件很简单,实现位于 ConcurrentMarkSweepThread 类中,相当于Java 中的Thread,该线程随着堆一起初始化,在该类的 run 方法中有这么一段逻辑:
1 | while (!_should_terminate) { |
sleepBeforeNextCycle()
保证了最晚每 2 秒(-XX:CMSWaitDuration)进行一次判断,实现如下:
1 | void ConcurrentMarkSweepThread::sleepBeforeNextCycle() { |
其中shouldConcurrentCollect()
方法决定了是否可以触发本次 cms gc,分为以下几种情况:
-
如果
_full_gc_requested
为真,说明有明确的需求要进行gc,比如调用System.gc()
; -
CMS 默认采用 jvm 运行时的统计数据判断是否需要触发 cms gc,如果需要根据
CMSInitiatingOccupancyFraction
的值进行判断,需要设置参数-XX:+UseCMSInitiatingOccupancyOnly
-
如果开启了
UseCMSInitiatingOccupancyOnly
参数,判断当前老年代使用率是否大于阈值,则触发 cms gc,该阈值可以通过参数-XX:CMSInitiatingOccupancyFraction
进行设置,如果没有设置,默认为92% -
如果之前的 ygc 失败过,或则下次新生代执行 ygc 可能失败,这两种情况下都需要触发 cms gc;
-
CMS 默认不会对永久代进行垃圾收集,如果希望对永久代进行垃圾收集,需要设置参数
-XX:+CMSClassUnloadingEnabled
如果开启了CMSClassUnloadingEnabled,根据永久带的内存使用率判断是否触发 cms gc; -
…还有一些其它情况
如果有上述几种情况,说明需要执行一次 cms gc,通过调用_collector->collect_in_background(false, cause)
进行触发,注意这个方法名中的in_background
FULL GC 如何触发
触发 full gc 的主要原因是在eden区为对象或TLAB分配内存失败,导致一次 ygc,在 GenCollectorPolicy 类的satisfy_failed_allocation()方法中有这么一段逻辑:
1 | if (!gch->incremental_collection_will_fail(false /* don't consult_young */)) { |
该方法是由 vm thread 执行的,整个过程都是 stop-the-world,如果当前incremental_collection_will_fail
方法返回 false,则会放弃本次的 ygc,直接触发一次 full gc,incremental_collection_will_fail
实现如下:
1 | bool incremental_collection_will_fail(bool consult_young) { |
其中参数 consult_young
为 false
,如果incremental_collection_failed()
返回 true
,会导致执行很慢很慢很慢的full gc,如果上一次 ygc 过程中发生 promotion failure
时,会设置 _incremental_collection_failed
为 true
,即方法incremental_collection_failed()
返回 true,相当于触发了 full gc。
还有一种情况是,当发生ygc之后,还是没有足够的内存进行分配,这时会继续触发 full gc,实现如下:
1 | // If we reach this point, we're really out of memory. Try every trick |
FULL GC 中的compact
每次触发 full gc,会根据should_compact
标识进行判断是否需要执行 compact ,判断实现如下:
1 | *should_compact = |
UseCMSCompactAtFullCollection
默认开启,但是否要进行 compact,还得看后面的条件:
- 最近一次cms gc 以来发生 full gc 的次数
_full_gcs_since_conc_gc
(这个值每次执行完 cms gc 的sweeping 阶段就会设置为0)达到阈值CMSFullGCsBeforeCompaction
。 - 用户强制执行了gc,如
System.gc()
。 - 上一次 ygc 已经失败(发生了promotion failure),或预测下一次 ygc 不会成功。
如果上述条件都不满足,是否就一直不进行 compact,这样碎片问题就得不到缓解了,幸好还有补救的机会,实现如下:
1 |
|
普通的 full gc,参数clear_all_soft_refs
为 false,不会清理软引用,如果在执行完 full gc,空间还是不足的话,会执行一次彻底的 full gc,尝试清理所有的软引用,想方设法的收集可用内存,这种情况clear_all_soft_refs
为 true,而且CMSCompactWhenClearAllSoftRefs
默认为 true,在垃圾收集完可以执行一次compact,如果真的走到了这一步,该好好的查查代码了,因为这次 gc 的暂停时间已经很长很长很长了。
根据对should_compact
参数的判断,执行不同的算法进行 full gc,实现如下:
1 | if (should_compact) { |
关于引用和和gc 处理 将会在下节讨论
参考:
关于CMS垃圾收集算法的一些疑惑
Java垃圾回收浅析(2)-GC方式介绍
赞赏一下