触发Full GC的时机

时间:2022-05-15 17:20:20

由于Full GC的耗时是Minor GC的十倍左右,所以Full GC的频率设计得比Minor GC低得多。现总结一下触发Full GC的情况。

在那些实现了CMS的比较新的虚拟机中,如果配置了-XX:+UseConcMarkSwapGC,则启用CMS回收算法,CMS会周期性地检查老年代的情况,每隔一定时间(默认2秒),就检查是否需要对老年代进行一次CMS回收,判断的依据如下:

1、如果没有设置-XX:+UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(建议线上环境带上这个参数,不然会加大问题排查的难度)。
2、老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%。
3、永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled
4、新生代的晋升担保失败。

CMS分为两种模式,background和foreground,background采用concurrent remark模式,可以和用户进程并行,而foreground则必须要stop the world(STW)。周期性的CMS采用的是background的方式,而主动的GC则采用foreground方式。主动的GC肯定是Full GC,反之则未必,因为Full GC不是只在CMS中存在的,并且Full GC也可以是并行的Full GC(区别于正常的Full GC),采用并行的Full GC在Old GC阶段走的是background式的CMS,可参考寒泉子《jvm源码分析之SystemGC完全解读》一文。本文的重点也是Full GC。

因为Full GC所用的时间较长,为了充分发挥CMS的优势,通常都会配置-XX:+UseCMSInitiatingOccupancyOnly,让CMS周期性的以和用户现场并行的方式进行垃圾回收,这也是CMS设计的初衷。

但是呢,因为CMS采用标记-清理的方式进行GC,所以会产生碎片,时间久了,碎片就会很多,所以一般来说,进行了一段时间CMS之后,如果开启了UseCMSCompactAtFullCollection(默认为true开启),在foreground的时候就要采用一次压缩,这时采用Serial Old或Parallel Old这些采用标记-整理算法的GC方式(CMS采用的是标记-清理算法)进行回收。具体多少次full gc (foreground CMS)之后进行一次压缩,取决于-XX:CMSFullGCsBeforecompact(默认为0)。源码如下:

*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 */)
);

从源码可以看出,除了达到一定次数之外,如果用户调用了System.gc()以及发生了promotion failed,也会进行一次压缩。同时也可以看出,foreground不一定会采用压缩,所以那些说foreground就是mark swap compact(msc)的是不对的。

但是周期性的CMS(background)只会回收老年代,除了周期性的进行GC之外,还有一些紧急情况,需要主动触发GC(foreground),主动触发的GC会连带一次Minor GC,所以也称为Full GC。主动触发的GC是会暂停所有用户现场的,俗称stop the world(STW)。

在那些没有实现CMS的老虚拟机或者没有开启CMS的虚拟机中,每一次Old GC都是Full GC,且会STW,当然因为除了CMS之外,其他的老年代回收算法都是采用标记-整理的方式,所以肯定也是压缩的,。

如果正在进行CMS回收,又触发了一次Full GC,则Full GC会抢占回收执行机会,停止CMS,采用Serial Old或Parallel Old这些采用标记-整理算法的GC方式(CMS采用的是标记-清理算法)进行Full GC。

下面是一些会触发Full GC的情况

1.System.gc()

在没有开启DisableExplicitGC的情况下,虽然只是建议,但是很多情况下,都会调用Full GC的,比如在原本应进行CMS的时候。System.gc()一般都是用在要释放堆外内存的时候使用。

2.老生代内存不足的时候

这种情况通常是对象要晋升到老年代中时,发现老年代的内存不足了,所以要引发一次Full GC。对象的晋升分为正常晋升和提前晋升。

3.永生区空间不足时

有些虚拟机把方法区也放到堆中管理,当加载的类太多时,永生区内存不足需要回收,也会触发Full GC

4.冒险失败之后

在Minor GC时发现to去的内存不足,则将Eden区和from区的内存全部晋升到老年区,清空新生代。但是如果此时老年区内存不足,则会冒险失败,冒险失败之后,对象仍然留在新生代(此时的Eden区和from区都接近99%),然后出发一次Full GC,这样便于下次如果还有冒险,可以增加冒险成功的几率。

5.HandlePromotionFailure设置为false或者历年平均晋升对象的大小大余老年代剩余连续空间

在Minor GC之前,虚拟机会检查老年代剩余连续空间是否大余新生代所有对象总大小,如果大余,则说明Minor GC绝对安全;如果小于,则会检查HandlePromotionFailure设置是否担保失败,如果不担保,则在Minor GC之前进行一次Full GC;如果担保,则再检查历年平均晋升对象的大小是否大余老年代剩余连续空间,如果大余,则不冒险,在Minor GC之前进行一次Full GC;如果小于,则冒险,进入情况4。

6.分配大对象时(和2其实是一样的,但是因为特殊,所以单独拿出来说)

如果直接要分配一个大对象,并且这个大对象的大小超过Eden区的一半,这个对象就会直接分配在老年代,此时如果老年代空间不足,出发一次Full GC,而不出发Minor GC。但是需要注意的是,如果分配的是TLAB而不是真正的大对象,那么不会导致full gc,而是调整TLAB的大小。

7.执行jmap -histo:live或者jmap -dump:live的时候

这属于强制让虚拟机执行一次full GC。