jvm源码阅读笔记[1]:如何触发一次CMS回收

本文详细分析了CMS垃圾收集器的工作原理,特别是如何触发一次CMS回收。CMS线程每2秒检查老年代是否需要回收,依据CMSWaitDuration参数决定。触发条件包括:使用率超过阈值、系统请求、空间不足等。CMSInitiatingOccupancyFraction配置默认为92%,未设置时会自动计算。文章提供源码地址供进一步研究。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
    个人源码地址:https://github.com/FlashLightNing/openjdk-notes
    还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk
    jvm源码阅读笔记[1]:如何触发一次CMS回收
    jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解
    jvm源码阅读笔记[3]:从内存分配到触发GC的细节
    jvm源码阅读笔记[4]:从GC说到vm operation
    jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?

    对于配置使用CMS回收器的应用,用jstack pid | grep GC 可以发现,有一个名为Concurrent Mark-Sweep GC Thread的线程。
    

    
    这个线程的作用呢,就是每隔2秒钟,就检查一下老年代是否需要回收,如果需要,则就会进行一次CMS回收。为什么这么说呢?来看看这个线程对应的源码:源码地址
    

ConcurrentMarkSweepThread::ConcurrentMarkSweepThread(CMSCollector* collector)
  : ConcurrentGCThread() {
  assert(UseConcMarkSweepGC,  "UseConcMarkSweepGC should be set");//表示在vm启动参数中必须加上UseConcMarkSweepGC
  assert(_cmst == NULL, "CMS thread already created");
  _cmst = this;
  assert(_collector == NULL, "Collector already set");
  _collector = collector;
//前面的这些assert主要是一些参数判断,保证这些参数符合条件。
  set_name("Concurrent Mark-Sweep GC Thread");//可以看到在这设置了线程名称

  if (os::create_thread(this, os::cgc_thread)) {
    int native_prio;
    if (UseCriticalCMSThreadPriority) {//UseCriticalCMSThreadPriority默认false
      native_prio = os::java_to_os_priority[CriticalPriority];
    } else {
      native_prio = os::java_to_os_priority[NearMaxPriority];
    }
    os::set_native_priority(this, native_prio);

    if (!DisableStartThread) {
      os::start_thread(this);
    }
  }
  _sltMonitor = SLT_lock;
  assert(!CMSIncrementalMode || icms_is_enabled(), "Error");
}

    
    再来看看比较关键的线程的run()方法:
    

//线程的run方法
void ConcurrentMarkSweepThread::run() {
  assert(this == cmst(), "just checking");

  this->record_stack_base_and_size();
  this->initialize_thread_local_storage();
  this->set_active_handles(JNIHandleBlock::allocate_block());
  // 检查是否为当前线程
  assert(this == Thread::current(), "just checking");
  if (BindCMSThreadToCPU && !os::bind_to_processor(CPUForCMSThread)) {
    warning("Couldn't bind CMS thread to processor " UINTX_FORMAT, CPUForCMSThread);
  }
  {
    CMSLoopCountWarn loopX("CMS::run", "waiting for "
                           "Universe::is_fully_initialized()", 2);
    MutexLockerEx x(CGC_lock, true);
    set_CMS_flag(CMS_cms_wants_token);
    //阻塞到堆初始化完成且所有初始化工作都做完了
    while (!is_init_completed() && !Universe::is_fully_initialized() &&
           !_should_terminate) {
      CGC_lock->wait(true, 200);
      loopX.tick();
    }
    CMSLoopCountWarn loopY("CMS::run", "waiting for SLT installation", 2);
    while (_slt == NULL && !_should_terminate) {
      CGC_lock->wait(true, 200);
      loopY.tick();
    }
    clear_CMS_flag(CMS_cms_wants_token);
  }
//_should_terminate变量只有在stop方法中才会为true,所以只要没有调用stop方法,这个就是while死循环,一直会检测
  while (!_should_terminate) {
    sleepBeforeNextCycle();//该方法内部也是一个while循环,只有在_should_terminate=true或者是需要进行一次GC时才会结束该方法然后往下运行。具体看后面的分析
    if (_should_terminate) break;

  //判断此次GC的类型
    GCCause::Cause cause = _collector->_full_gc_requested ?
      _collector->_full_gc_cause : GCCause::_cms_concurrent_mark;
    _collector->collect_in_background(false, cause);//进行background回收
  }
  assert(_should_terminate, "just checking");
  verify_ok_to_terminate();
  {
    MutexLockerEx mu(Terminator_lock,
                     Mutex::_no_safepoint_check_flag);
    assert(_cmst == this, "Weird!");
    _cmst = NULL;
    Terminator_lock->notify();
  }

  // Thread destructor usually does this..
  ThreadLocalStorage::set_thread(NULL);
}

    
其中,关键的是调用的sleepBeforeNextCycle()方法:
    

void ConcurrentMarkSweepThread::sleepBeforeNextCycle() {
  while (!_should_terminate) {
    if (CMSIncrementalMode) {//CMS增量回收模式,默认false
      icms_wait();
      if(CMSWaitDuration >= 0) {
        wait_on_cms_lock_for_scavenge(CMSWaitDuration); 
      }
      return;
    } else {
      //CMSWaitDuration 默认是2000
      if(CMSWaitDuration >= 0) {
       wait_on_cms_lock_for_scavenge(CMSWaitDuration);//该方法类似于j.u.c里面while循环+超时等待,会阻塞线程CMSWaitDuration毫秒时间
      } else {
        wait_on_cms_lock(CMSCheckInterval);
      }
    }
    //根据一定条件判断是否需要进行一次CMSGC
    if (_collector->shouldConcurrentCollect()) {
      return;
    }
  }
}

    根据CMSWaitDuration参数,我们可以知道,该cms线程会一直在sleepBeforeNextCycle()方法内循环,然后阻塞完2000毫秒后,判断是否需要进行一次GC。如果不需要,继续走上面的流程。如果需要,就结束sleepBeforeNextCycle()方法,然后在run方法中,进行一次_collector->collect_in_background()调用,从而回收老年代。
    
    需要注意的是,这里进行的是background式的CMS GC。还有一种是foreground式的CMS GC.具体它们的区别,可以参考这篇文章:http://lovestblog.cn/blog/2015/05/07/system-gc/
    
    所以总结起来就是:当堆初始化完成后,就会创建一个cms的 gc线程。这个线程的作用,就是根据配置的CMSWaitDuration时间(默认2000ms),轮询判断老年代是否需要进行GC(根据一定的条件,比如占用率达到阈值)。如果需要GC,则进行一次background式的CMS GC。

    那么,再来看看满足哪些条件的时候jvm会认为需要进行一次CMS GC呢?关键看看concurrentMarkSweepGeneration.cpp中的shouldConcurrentCollect()方法。(concurrentMarkSweepGeneration.cpp地址

//方法开始:判断是否要进行并发回收
bool CMSCollector::shouldConcurrentCollect() {
  if (_full_gc_requested) {//发起了一次full gc时
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print_cr("CMSCollector: collect because of explicit "
                             " gc request (or gc_locker)");
    }
    return true;
  }

  NOT_PRODUCT(
    if (RotateCMSCollectionTypes &&
        (_cmsGen->debug_collection_type() !=
          ConcurrentMarkSweepGeneration::Concurrent_collection_type)) {
      assert(_cmsGen->debug_collection_type() !=
        ConcurrentMarkSweepGeneration::Unknown_collection_type,
        "Bad cms collection type");
      return false;
    }
  )

  FreelistLocker x(this);
  // Print out lots of information which affects the initiation of a collection.
  if (PrintCMSInitiationStatistics && stats().valid()) {
    gclog_or_tty->print("CMSCollector shouldConcurrentCollect: ");
    gclog_or_tty->stamp();
    gclog_or_tty->cr();
    stats().print_on(gclog_or_tty);
    gclog_or_tty->print_cr("time_until_cms_gen_full %3.7f",
      stats().time_until_cms_gen_full());
    gclog_or_tty->print_cr("free="SIZE_FORMAT, _cmsGen->free());
    gclog_or_tty->print_cr("contiguous_available="SIZE_FORMAT,
                           _cmsGen->contiguous_available());
    gclog_or_tty->print_cr("promotion_rate=%g", stats().promotion_rate());
    gclog_or_tty->print_cr("cms_allocation_rate=%g", stats().cms_allocation_rate());
    gclog_or_tty->print_cr("occupancy=%3.7f", _cmsGen->occupancy());
    gclog_or_tty->print_cr("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy());
    gclog_or_tty->print_cr("cms_time_since_begin=%3.7f", stats().cms_time_since_begin());
    gclog_or_tty->print_cr("cms_time_since_end=%3.7f", stats().cms_time_since_end());
    gclog_or_tty->print_cr("metadata initialized %d",
      MetaspaceGC::should_concurrent_collect());
  }

  /* 
  UseCMSInitiatingOccupancyOnly默认false
  如果并不是只有在占用率达到要求的时候才回收,则会动态进行计算
  如果预计的完成CMS回收的所需要的时间小于预计的老年代填满的时间,则进行回收。
  */
  if (!UseCMSInitiatingOccupancyOnly) {
    if (stats().valid()) {
      if (stats().time_until_cms_start() == 0.0) {
        return true;
      }
    } else {
      if (_cmsGen->occupancy() >= _bootstrap_occupancy) {//_bootstrap_occupancy默认50%
        if (Verbose && PrintGCDetails) {
          gclog_or_tty->print_cr(
            " CMSCollector: collect for bootstrapping statistics:"
            " occupancy = %f, boot occupancy = %f", _cmsGen->occupancy(),
            _bootstrap_occupancy);
        }
        return true;
      }
    }
  }


  if (_cmsGen->should_concurrent_collect()) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print_cr("CMS old gen initiated");
    }
    return true;
  }

  /* 
  如果认为一个增量式的回收可能失败,我们就会开始一次回收。这在实践中不太可能生效,因为这时候已经太迟了。
  */
  GenCollectedHeap* gch = GenCollectedHeap::heap();
  assert(gch->collector_policy()->is_two_generation_policy(),
         "You may want to check the correctness of the following");
  if (gch->incremental_collection_will_fail(true /* consult_young */)) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print("CMSCollector: collect because incremental collection will fail ");
    }
    return true;
  }

  if (MetaspaceGC::should_concurrent_collect()) {//检查metaspace是否需要GC
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print("CMSCollector: collect for metadata allocation ");
    }
    return true;
  }

  // CMSTriggerInterval starts a CMS cycle if enough time has passed.
  //CMS的触发间隔,默认-1.
  if (CMSTriggerInterval >= 0) {
    if (CMSTriggerInterval == 0) {
      // Trigger always
      return true;
    }

    if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) {
      if (Verbose && PrintGCDetails) {
        if (stats().valid()) {
          gclog_or_tty->print_cr("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)",
                                 stats().cms_time_since_begin());
        } else {
          gclog_or_tty->print_cr("CMSCollector: collect because of trigger interval (first collection)");
        }
      }
      return true;
    }
  }

  return false;
}
//方法结束

    这个方法比较长,主要有以下几种方式触发:

(1) 请求进行一次fullgc,如调用System.gc时
(2) 当没有设置UseCMSInitiatingOccupancyOnly时,会动态计算。如果完成CMS回收的所需要的预计的时间小于预计的CMS回收的分代填满的时间,就进行回收
(3) 调用should_concurrent_collect()方法返回true(后面会讲都有哪些情况时该方法会返回true)
(4) 如果预计增量式回收会失败时,也会触发一次回收。
(5) 如果metaSpace认为需要回收metaSpace区域,也会触发一次cms回收
(6) 如果设置了CMSTriggerInterval参数。该参数默认值-1,表示CMS回收的间隔。如果=0表示一直触发CMSGC。如果>0,则会判断从开始CMSGC到现在经过的时间是否大于该参数的时间。若大于,则会进行一次CMSGC。
如果都不满足,则返回false,表示不满足进行GC的条件。

    再来看看第3条,看看什么时候should_concurrent_collect()会返回true。
    

bool ConcurrentMarkSweepGeneration::should_concurrent_collect() const {

  assert_lock_strong(freelistLock());
  if (occupancy() > initiating_occupancy()) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because of occupancy %f / %f  ",
        short_name(), occupancy(), initiating_occupancy());
    }
    return true;
  }
  /*
  如果使用UseCMSInitiatingOccupancyOnly参数,表示只有在使用率超过配置的情况下的时候
  才会进行CMS回收。
  如果没有使用-XX:+UseCMSInitiatingOccupancyOnly,
  那么HotSpot VM只是利用这个值来启动第一次CMS垃圾回收,后面都是使用HotSpot VM自动计算出来的值。
  */
  if (UseCMSInitiatingOccupancyOnly) {
    return false;
  }
  if (expansion_cause() == CMSExpansionCause::_satisfy_allocation) {//扩容
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because expanded for allocation ",
        short_name());
    }
    return true;
  }
  //这里的_cmsSpace是指CompactibleFreeListSpace
  if (_cmsSpace->should_concurrent_collect()) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because cmsSpace says so ",
        short_name());
    }
    return true;
  }
  return false;
}

    
    可以发现,当
    (1)堆当前的使用率>配置的initiating_occupancy,也就是我们经常配置的-XX:CMSInitiatingOccupancyFraction=80。
    (2)当设置了UseCMSInitiatingOccupancyOnly=true,也就是只允许使用率达到阈值才进行回收时,不会判断其他条件。
    (3)当堆扩容的原因是_satisfy_allocation时。
    (4)CompactibleFreeListSpace认为需要回收时。

    
    总结起来,就是下面这么多原因:

    (1)请求进行一次fullgc,如调用System.gc时
    (2)当没有设置UseCMSInitiatingOccupancyOnly时,会动态计算。如果完成CMS回收的所需要的预计的时间小于预计的CMS回收的分代填满的时间,就进行回收
    (3.1)堆当前的使用率>配置的initiating_occupancy,也就是我们经常配置的-XX:CMSInitiatingOccupancyFraction=80。
    (3.2)当设置了UseCMSInitiatingOccupancyOnly=true,也就是只允许使用率达到阈值才进行回收时,不会判断其他条件。
    (3.3)当堆扩容的原因是_satisfy_allocation时。
    (3.4)CompactibleFreeListSpace认为需要回收时。
    (4)如果预计增量式回收会失败时,也会触发一次回收。
    (5)如果metaSpace认为需要回收metaSpace区域,也会触发一次cms回收
    (6)如果设置了CMSTriggerInterval参数。该参数默认值-1,表示CMS回收的间隔。如果=0表示一直触发CMSGC。如果>0,则会判断从开始CMSGC到现在经过的时间是否大于该参数的时间。若大于,则会进行一次CMSGC。

    这里还需要说明的一点是-XX:CMSInitiatingOccupancyFraction配置的默认值是-1.当它是-1的时候,会通过concurrentMarkSweep.cpp中的init_initiating_occupancy(intx io, uintx tr)方法计算。
    

void ConcurrentMarkSweepGeneration::init_initiating_occupancy(intx io, uintx tr) {
  //tr为CMSTriggerRatio参数,表示触发CMS的比例,默认80%,。IO表示CMSInitiatingOccupancyFraction,即使用率,默认-1
  assert(io <= 100 && tr <= 100, "Check the arguments");
  if (io >= 0) {
    _initiating_occupancy = (double)io / 100.0;
  } else {//所以默认没有配置的情况下,会走下面的逻辑,MinHeapFreeRatio默认40
    _initiating_occupancy = ((100 - MinHeapFreeRatio) +
                             (double)(tr * MinHeapFreeRatio) / 100.0)
                            / 100.0;
  }
}

    
    计算得到_initiating_occupancy=(100-40+80*40/100)/100=92%。也就是说,如果不配置CMSInitiatingOccupancyFraction参数,默认情况下,initiating_occupancy的值就是92%。
    

    从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
    个人源码地址:https://github.com/FlashLightNing/openjdk-notes
    还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk
    jvm源码阅读笔记[1]:如何触发一次CMS回收
    jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解
    jvm源码阅读笔记[3]:从内存分配到触发GC的细节
    jvm源码阅读笔记[4]:从GC说到vm operation
    jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值