1. CMS 如何工作的?

  • CMS 采用“标记清理”算法
  • CMS 在执行一次垃圾回收的过程一共分为 4 个阶段(GC 之后可以设置内存碎片整理):
    • 初始标记
      • 进入 Stop the World 状态,系统工作线程全部停止;
      • 标记出所有 GC Roots 直接引用的对象:
        • 方法的局部变量和类的静态变量是 GC Roots,但类的实例变量不是 GC Roots;
      • 初始标记的速度很快,Stop the World 很短,对系统工作线程的影响不大;
    • 并发标记
      • 允许系统工作线程继续运行,可能会向老年代转入新的存活对象,也可能部分存活对象变成垃圾对象;
      • 会尽可能的对老年代里所有的对象进行 GC Roots 追踪,追踪所有的对象是否从根源上被 GC Roots 引用了,如果没有被 GC Roots 间接引用则标记为不可回收;
      • GC Roots 追踪是最耗时的,但因为跟系统工作线程并发运行的,所以并发标记对系统运行不会造成影响;
    • 重新标记
      • 进入 Stop the World 状态,系统工作线程全部停止;
      • 针对并发标记阶段中新转入的对象和一些失去引用变成垃圾的对象,重新进行标记;
      • 重新标记的速度很快,因为只针对被系统程序运行变动过的少数对象;
    • 并发清理
      • 允许系统工作线程继续运行,并发清理期间,可能会向老年代转入新的存活对象,也可能部分存活对象变成垃圾对象,这些对象只能等待下一次 GC 进行标记处理;
      • 清理掉之前标记为垃圾的对象;
      • 并发清理阶段是很耗时的,但因为跟系统工作线程并发运行的,所以不影响系统程序的执行;
    • 内存碎片整理
      • 使用 -XX:+UseCMSCompactAtFullCollection 参数,在 Full GC 后会进行内存碎片整理,默认是打开了的;
      • 使用 -XX:CMSFullGCsBeforeCompaction 参数,可以设置执行多少次 Full GC 之后再执行一次内存碎片整理的工作,默认值 0,默认就是每次 Full GC 之后都会进行一次内存整理;
      • 内存整理时,会进入 Stop the World 状态;
        1. public class Kafka{
        2. private static ReplicaManager replicaManager = new ReplicaManager();
        3. }
        4. public class RepicaManager {
        5. private ReplicaFetcher replicaFetcher = new ReplicaFetcher();
        6. }
        image.png

2. 对 CMS 的垃圾回收机制进行性能分析

  • 最耗时的阶段,都是和系统程序并发执行的,所以基本这两个最耗时的阶段对性能影响不大:
    • 第二阶段,并发标记,是对老年代全部对相关进行 GC Roots 追踪;
    • 第四阶段,并发清理,是对各种垃圾对象从内存里清理掉;
  • 需要“Stop the World”的阶段,都是简单的标记而已,速度非常的快,所以基本上对系统运行响应也不大。
    • 第一阶段,初始标记;
    • 第三阶段,重新标记;

3. CMS 垃圾回收期间会出现的一些细节问题

(1) 并发垃圾回收导致 CPU 资源紧张

  • 两个最耗时的阶段:并发标记、并发清理,垃圾回收线程会和系统工作线程同时工作,会导致有限的 CPU 资源被垃圾回收线程占用一部分;
  • CMS 默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4;
    • 例如,2核4G 机器,CMS 会有 (2+3)/4 = 1 个垃圾回收线程,去占用了一个宝贵的 CPU;
  • CMS 机制的第一个问题,就是会消耗 CPU 资源。

(2) Concurrent Mode Failure 问题

  • 并发清理阶段,系统工作线程继续运行,会在老年代里产生一些浮动对象,无法立即回收它们;
    • 浮动对象:新生代一些存活对象转入老年代,很快这些对象又没人引用,变成了垃圾对象,这些垃圾对象在并发清理阶段未被标记,等待下一次 GC 回收它们;
  • 为了保证并发清理期间,还有一些空间让系统线程转入一些对象进入老年代,一般会预留一些可用空间;
    • CMS 垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC,这样在并发清理阶段就会预留出一个可用内存空间;
    • -XX:CMSInitiatingOccupancyFaction”参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%,即预留8%的内存给并发清理阶段,让系统线程把一些对象从新生代传入老年代。
  • CMS 并发清理阶段,系统工作线程要转入的对象大于老年代预留的可用空间,就会发生 Concurrent Mode Failure 问题:并发垃圾回收失败,预留内存不够,不能一边回收垃圾,一边把对象放入老年代;
  • 自动使用 Serial Old 垃圾回收器替代 CMS,强行进入 Stop the World,重新进行长时间的 GC Roots 追踪,标记出全部垃圾对象,不允许新对象产生,然后一次性把垃圾对象清理掉,最后恢复系统工作线程运行;
  • CMS 机制的第二个问题,就是需要合理优化这个自动触发 CMS 垃圾回收的比例,老年代预留出合适的可用空间,避免 Concurrent Mode Failure 问题。

(3) 内存碎片问题

  • CMS 在并发清理阶段后,会又大量内存碎片产生,导致无法找到连续内存空间存放对象,进而频繁触发 Full GC;
  • 使用“-XX:+UseCMSCompactAtFullCollection”,GC 后会内存碎片整理。