CMS和FULL GC

内容纲要

前言

CMS是JDK1.8之前非常重要的一个GC方式,通常会和FULL GC一起谈论,有FULL GC就有CMS。那么,CMS等于FULL GC吗?答案是:不是。

FULL GC介绍

我们都很熟知JAVA内存的分代模型,那为什么要采用分代模型呢?最主要的原因就是基于内存管理的需求。JAVA的分代模型其实是基于一个假设(其实叫假设不准确,这是从实际Java应用的内存使用观察得到的结论):

90%的对象熬不过第一次垃圾回收,而老的对象(经历了好几次垃圾回收的对象)则有98%的概率会一直活下来。

基于这个假设,一般的垃圾回收器把内存分成三类: Eden(E), Suvivor(S)和Old(O), 其中Eden和Survivor都属于年轻代,Old属于老年代,新对象始终分配在Eden里面,熬过一次垃圾回收的对象就被移动到Survisor区了,经过数次垃圾回收之后还活着的对象会被移到old区。针对不同的分代空间,可以灵活的管理该空间的内存,比如:垃圾回收。
从分代概念来说,JAVA的GC主要发生在年轻代和老年代,也就是Minor GC和Majar GC。

Minor GC

新生代GC,指发生在新生代的垃圾收集动作,清理整合YouGen的过程, eden 的清理,S0\S1的清理都由于MinorGC完成。Minor GC常用标记和复制算法,每次清理时都是直接将S0\S1的对象直接清除,所以 Eden 和 Survivor 区不存在内存碎片
所有的Minor GC都会触发全世界的暂停(stop-the-world),停止应用程序的线程,通常情况下,停顿导致的延迟都是可以忽略不计的。。其中的真相就是,大部分Eden区中的对象都能被认为是垃圾,永远也不会被复制到Survivor区或者老年代空间。如果正好相反,Eden区大部分新生对象不符合GC条件,Minor GC 执行时暂停的时间将会长很多。

Major GC

老年代GC,指发生在老年代的GC。
大多情况下,Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的。

FULL GC

针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。
简单理解就是FULL GC= Minor GC + Major GC。

CMS介绍

现在说说什么是CMS,Mostly-Concurrent收集器,也称并发标记清除收集器(Concurrent Mark-Sweep GC,CMS收集器),是一款并发的、使用标记-清除算法的垃圾回收器。CMS的设计目的是为了消除Throught收集器和Serial收集器在Full GC周期中的长时间停顿。所以CMS只是垃圾收集器,只是通常我们使用CMS作为老年代的垃圾回收器,所以说CMS不等于FULL GC。

周期性CMS GC(被动)

周期性CMS GC,执行的逻辑也叫 BackgroundCollect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。
触发条件:

  • 如果没有设置 UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(如果没有这个参数,虚拟机会根据运行情况自动调整触发比例,也就是CMSInitiatingOccupancyFraction参数的配置只在初始化时有用)
  • 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%
  • 永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled
  • 新生代的晋升担保失败

这里再额外说一下,什么是晋升担保失败?
老年代是否有足够的空间来容纳全部的新生代对象或历史平均晋升到老年代的对象,如果不够的话,就提早进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败。

主动CMS GC

主动CMS GC的过程,触发条件比较苛刻:

  • YGC过程发生Promotion Failed,进而对老年代进行回收
  • System.gc(),前提是添加了-XX:+ExplicitGCInvokesConcurrent参数。

如果触发了主动CMS GC,这时周期性CMS GC正在执行,那么会夺过周期性CMS GC的执行权(同一个时刻只能有一种在CMS GC在运行),并记录 concurrent mode failure 或者 concurrent mode interrupted。
主动GC开始时,需要判断本次GC是否要对老年代的空间进行Compact(因为长时间的周期性GC会造成大量的碎片空间),在三种情况下会进行压缩:

  • 其中参数 UseCMSCompactAtFullCollection(默认true)和CMSFullGCsBeforeCompaction(默认0),所以默认每次的主动GC都会对老年代的内存空间进行压缩,就是把对象移动到内存的最左边。
  • 执行了 System.gc(),也会进行压缩。
  • 新生代的晋升担保会失败。

这里需要注意的是,触发了压缩动作,应用程序将会暂停很长很长时间。

总结

如果我们要提高应用程序在GC时的STW时间,应该尽量避免FULL GC的触发,特别是主动CMS GC的触发。
警惕Promotion Failed。Promotion Failed是在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的。但是你查看监控,你可能会发现内存其实还有很多,那往往就是内存碎片造成,新生代要转移到老年代的对象比较大,找不到一段连续区域存放这个对象导致。
Promotion Failed触发的一些原因分析:

  • 过早的提升和提升失败。在 Minor GC 过程中,Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为过早提升(Premature Promotion),这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 再进一步, 如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆, 称为提升失败(Promotion Failure)
  • 如果是因为提升过快导致的,说明Survivor 空闲空间不足,那么可以尝试调大 Survivor;
  • 如果是因为老年代空间不够导致的,尝试将CMS触发的阈值调低;

另外还有一个需要注意的地方是系统资源导致的,可以观测GC日志中的Times时间,通过sys和user以及real时间来判断是否是因为系统资源导致。例如:user和sys都很小,但是real很长,那就是IO导致的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注