美文网首页
CMS算法实现 - 2

CMS算法实现 - 2

作者: 程序员札记 | 来源:发表于2023-01-21 10:02 被阅读0次

一、Young GC
Young GC就是年轻代的GC,由VMThread在安全点下执行,相关实现在hotspot\src\share\vm\memory\defNewGeneration.cpp中,整个过程都是单线程执行的。如果UseParNewGC为true,该属性默认为false,则GC时部分环节如根节点遍历是并行的,但主流程和单线程时是完全一样的,相关实现在hotspot\src\share\vm\gc_implementation\parNew\asParNewGeneration.cpp中。下面的讨论以单线程的DefNewGeneration的实现为准。

1、should_collect
DefNewGeneration采用父类Generation的实现,但改写了父类的should_allocate方法的实现,如下:

//full 表示是否Full GC
//word_size表示触发此GC的需要分配的内存大小
//is_tlab 表示待分配的内存是否用于TLAB
virtual bool should_collect(bool   full,
                              size_t word_size,
                              bool   is_tlab) {             
    return (full || should_allocate(word_size, is_tlab));
}
 
//返回是否应该在当前Genarationz中分配,默认配置下肯定返回true
virtual bool should_allocate(size_t word_size, bool is_tlab) {
    //如果UseTLAB为false,则is_tlab一定为false
    assert(UseTLAB || !is_tlab, "Should not allocate tlab");
    //BitsPerSize_t在64位下是64,LogHeapWordSize在64位下是3,现在的内存容量下word_size不可能大于overflow_limit
    size_t overflow_limit    = (size_t)1 << (BitsPerSize_t - LogHeapWordSize);
 
    const bool non_zero      = word_size > 0;
    const bool overflows     = word_size >= overflow_limit;
    //_pretenure_size_threshold_words初始化时被赋值成PretenureSizeThreshold >> LogHeapWordSize
    //PretenureSizeThreshold默认是0,表示无限制
    const bool check_too_big = _pretenure_size_threshold_words > 0;
    const bool not_too_big   = word_size < _pretenure_size_threshold_words;
    //只有三个条件都是false,size_ok才是false
    const bool size_ok       = is_tlab || !check_too_big || not_too_big;
 
    bool result = !overflows &&
                  non_zero   &&
                  size_ok;
 
    return result;
  }

从上述实现可知,TLAB应该在年轻代分配,full为true或者should_allocate返回true时则should_collect返回true,即FullGC或者应该在年轻代分配该内存时必须执行young GC。第二种场景是为啥呢?因为只有年轻代的内存不能满足要求才会调用此方法,如果该次分配应该在年轻代中分配,则必须执行young GC,从而保证有足够的空间满足分配,如果此时可以在老年代分配,则在老年代分配,从而延迟GC的触发。

2、gc_prologue
gc_prologue只是将eden区的_soft_end重置成end,让两者保持一致,其实现如下:

image.png

_soft_end主要用于内存分配场景,内存分配时不能超过该限制,其调用链如下:


image.png

该属性主要是给CMS的增量收集模式使用的,增量收集完成会重置这个属性,即允许在已经完成收集的内存区域中分配对象。增量收集模式通过参数CMSIncrementalMode控制,默认为false,主要适用于单CPU下的GC,避免长期GC影响了业务线程的正常执行,参考set_soft_end方法的调用链,如下:

image.png

其他的调用都是直接置为end。

3、collect
DefNewGeneration::collect方法比较长比较复杂,不贴源码了,其主要处理流程如下:


image.png

其中判断老年代空间是否充足的实现如下:

bool DefNewGeneration::collection_attempt_is_safe() {
  if (!to()->is_empty()) {
    //to区正常情况下是空,只有上一次因为老年代空间不足promote失败才会导致to区非空
    //所以这里直接返回false,终止young gc,等old gc完成有足够空间再来执行young gc
    return false;
  }
  if (_next_gen == NULL) {
    //初始化_next_gen
    GenCollectedHeap* gch = GenCollectedHeap::heap();
    _next_gen = gch->next_gen(this);
  }
  //used返回eden区和from区已使用内存的和,因为这两个区中的对象都可能往老年代promote
  return _next_gen->promotion_attempt_is_safe(used());
}
 
bool ConcurrentMarkSweepGeneration::promotion_attempt_is_safe(size_t max_promotion_in_bytes) const {
  size_t available = max_available();
  //历史的平均promote的对象的总大小
  size_t av_promo  = (size_t)gc_stats()->avg_promoted()->padded_average();
  bool   res = (available >= av_promo) || (available >= max_promotion_in_bytes);
}
 
size_t DefNewGeneration::used() const {
  return eden()->used()
       + from()->used();      // to() is only used during scavenge
}

相关补充说明如下:

  • 如果to区非空,则认为老年代没有充足空间执行promote,因为to区正常情况下都是空的,只有在因为老年代空间不足导致promote失败时才会非空。
  • 根节点遍历时会判断目标oop是否是年轻代的,如果是,则根据分代年龄将其拷贝到to区或者老年代,to区内存不足时会拷贝到老年代中,如果是拷贝到to区,会在对象复制完成后增加复制对象的分代年龄,最后让目标oop指向新的对象复制地址。注意拷贝到老年代的分代年龄的阈值是根据各年龄的对象总大小和to区的容量动态调整的,最大值是15。
  • 第二步遍历所有ClassLoaderData加载的Klass时,只遍历has_modified_oops返回true的Klass,即Klass对应的类class实例还在年轻代未promote到老年代的Klass
    注意出现promote失败并不会终止遍历,此时返回的地址还是原来的对象地址,因此引用类型属性实际还是指向原来的对象,另外会把promote失败的oop作为根节点,以跟第二步同样的方式不断遍历其所有的引用类型属性。
  • 遍历老年代脏的卡表项实际是遍历其对应内存区域中的对象,注意遍历前会先将脏的卡表项置为clean,如果该对象是存活的且不是promoted对象,则遍历该对象的同样在脏的卡表项对应的内存区域中的引用类型属性,即老年代存活对象所引用的年轻代的oop,会将该oop同样拷贝到to区或者老年代,让该引用类型属性指向对象的新复制地址,最后将该oop对应的卡表项置为youngergen_card。如果引用类型属性不在脏的内存区域中,说明该属性未发生变更,不需要处理。
  • 遍历所有promote到老年代的对象oop时,会恢复其对象头,并遍历其包含的所有引用类型属性,其引用类型属性指向的oop如果是年轻代的则同样将其拷贝到to区或者老年代,让该引用类型属性指向对象的新复制地址,最后将该oop对应的卡表项置为youngergen_card。
  • 只有第三步和第四步遍历引用类型属性时才会判断该oop是否是Reference实例,如果是则加入对应类型的待处理列表中。处理时会将referent对象还是存活的Reference实例从链表中移除,如果referent对象不是存活的但是需要保留,则将其作为根节点处理。
  • promote成功时交换to区和from区是为了保证to区是空的,在老年代执行压缩GC时会从to区分配空间;promote失败时交换to区和from区是为了保证from区是空的,从而提供一定的内存空间继续创建对象
    通知GCH promote失败后会触发CMSThread执行老年代的后台GC
  • 遍历eden区和from区的对象,恢复其对象头,去掉可能包含的对象复制地址,注意此时已经成功promote的对象,指向该对象的引用类型属性都已经成功指向新地址了,后续对象状态变更都是变更复制对象了,此时还留在eden区和from区的对象因为不好单独删除,所以暂且保留。而promote失败的对象,指向该对象的引用类型属性还是指向原来在eden区和from区的对象,这一步恢复对象头主要是为了让promoted失败的对象可以被正常访问。
  • 为啥遍历位于年轻代的根节点时不遍历其所引用的对象了?答案是年轻代的对象在创建时其引用类型属性肯定是null,设置属性时要么是new一个新对象,要么是使用老年代的对象,如果是前者,new的一个新对象肯定同样在根节点中,即位于年轻代的根节点,其所引用的年轻代对象也是在根节点oop中

4、gc_epilogue
其处理流程如下:

image.png

补充说明如下:

  1. 正常情况下只从eden区分配老年代对象,如果eden区内存不足了会触发年轻代GC,如果此时老年代空间不足promote失败,设置了从from区分配对象标识,则会从from区分配对象,尽可能的提高内存使用率
  2. ChunkPool的内存并不属于年轻代,这个是JVM分配一些需要经常创建和销毁的C++类使用的,只是在此处触发清理逻辑,从而释放空闲内存,其使用场景可以参考ChunkPool::allocate方法的调用链,部分截图如下:


    image.png

二、Old GC
CMS下老年代的GC分为前台GC和后台GC,前台GC是指内存分配失败或者通过System.gc()等由业务代码触发的GC,属于JVM的被动GC,通过VMThread执行,整个过程都在安全点下;后台GC是指由CMS Thread根据某些条件触发的GC,属于JVM的主动GC,其中只有InitialMarking和FinalMarking两个步骤通过VMThread执行且同样要求在安全点下执行,其他步骤无需在安全点下,即不需要STW。前台GC根据是否需要压缩又分为正常的GC和堆内存压缩式GC。后台GC和前台正常的GC都包含多个步骤,也可以理解成有多个状态,通过枚举CollectorState定义,如下:

image.png

其中Precleaning和AbortablePreclean是后台GC独有的,实际处理时会按照固定的顺序依次执行各个步骤,这些步骤的实现都在CMSCollector中,只不过对外的GC调用入口还是在表示老年代的ConcurrentMarkSweepGeneration中,堆内存压缩式GC由GenMarkSweep实现。

2.1 should_collect
should_collect的实现如下:

bool ConcurrentMarkSweepGeneration::should_collect(bool   full,
                                                   size_t size,
                                                   bool   tlab)
{
  //只有要求执行Full GC时才执行,正常情况下都是CMS Thread执行old GC
  return full || should_allocate(size, tlab); // FIX ME !!!
}
 
 virtual bool should_allocate(size_t word_size, bool is_tlab) {
    bool result = false;
 //BitsPerSize_t在64位下是64,LogHeapWordSize在64位下是3,现在的内存容量下word_size不可能大于overflow_limit
    size_t overflow_limit = (size_t)1 << (BitsPerSize_t - LogHeapWordSize);
    if (!is_tlab || supports_tlab_allocation()) {
      //只有非tlab下才会进入此逻辑
      result = (word_size > 0) && (word_size < overflow_limit);
    }
    return result;
  }
  
  //supports_tlab_allocation默认返回false,年轻代返回true
virtual bool supports_tlab_allocation() const { return false; }

如果tlab为true,该方法就返回false,即TLAB只能在年轻代中分配;非tlab下,该方法肯定返回true。

2.2 gc_prologue
其主要处理流程如下:

image.png

其中与set_accumulate_modified_oops方法对应的accumulate_modified_oops方法的调用链如下:


image.png

即只在年轻代GC时遍历Klass时会使用,KlassScanClosure的核心方法do_klass的实现如下:

void KlassScanClosure::do_klass(Klass* klass) {
  //如果是一个新加载的Klass,在初始化该Klass的java_mirror属性,即对应的类class实例时会将
  //modified_oops置为1,has_modified_oops返回true
  if (klass->has_modified_oops()) {
    if (_accumulate_modified_oops) {
      //将_accumulated_modified_oops置为1
      klass->accumulate_modified_oops();
    }
 
    //将_modified_oops恢复成0
    klass->clear_modified_oops();
 
    //通知_scavenge_closure准备扫描klass
    _scavenge_closure->set_scanned_klass(klass);
 
    //执行的过程中如果该Klass对应的java_mirror还在年轻代则将该klass的_modified_oops再次置为1
    //这样做的目的是确保下一次年轻代GC时还会将该对象作为存活对象处理,直到将其promote到老年代为止
    klass->oops_do(_scavenge_closure);
 
    _scavenge_closure->set_scanned_klass(NULL);
  }
}

与accumulate_modified_oops对应的has_accumulated_modified_oops方法的调用链如下:

image.png

前面两个是KlassRemSet使用的,第一个用于清除所有Klass的accumulated_modified_oops标识,后者用于判断是否有未清除的Klass。最后两个分别是preclean和finalmark步骤使用的,如果有这个标识会将klass的java_mirror作为根节点遍历其所有引用类型属性,以PrecleanKlassClosure的实现为例,如下:

image.png

另外set_accumulate_modified_oops的调用链如下,在gc_epilogue中将其置为false,在gc_prologue中将其置为true。


image.png

补充说明如下:

  • freeListLock实际是底层CMS Space的锁,从CMS Space分配内存, CMS Space扩容或者对象遍历时都需要获取该锁,参考其调用链,如下:


    image.png
  • bitMapLock就是操作用于记录对象存活状态的CMSBitMap的锁,注意CMSBitMap只在GC时使用,正常情况下就是空的,获取bitMapLock锁也都是GC的相关方法,其调用链如下:


    image.png

处于Marking期间是指当前GC的状态在Marking到Sweeping之间,不包括Sweeping, 因为只有在这期间才会遍历发生修改的klass。
设置脏的卡表项遍历的预处理遍历器,主要用于年轻代遍历脏的卡表项找到老年代引用的年轻代对象,在执行遍历前会先调用这个预处理器,将脏的卡表项对应的内存区域在CMSBitMap中打标。执行设置的setPreconsumptionDirtyCardClosure方法的调用链如下,gc_epilogue_work中将其置为NULL,gc_prologue_work将其设置成正确的遍历器。

设置accumulate_modified_oops和设置脏的卡表项遍历的预处理遍历器这两个都跟老年代GC和年轻代GC的交互相关,理解上述逻辑的关键在于负责触发年轻代或者老年代GC的GenCollectedHeap::do_collection方法,无论执行那种GC,年轻代和老年代的gc_prologue方法和gc_epilogue方法都会执行,而且是先执行年轻代的方法再执行老年代的方法,即只是单纯的young GC,上述两个属性也会被设置,这样做的目的是为了让老年代能够尽可能全面的感知到老年代的对象引用关系发生了修改。
最后一步填充LinearAllocBlock的目的是为了年轻代GC时promote小对象到老年代时给小对象快速分配内存

2.3 collect
其主要流程如下:

image.png

其中判断是否应该压缩堆内存,是否重新开始的方法实现如下:

void CMSCollector::decide_foreground_collection_type(
  bool clear_all_soft_refs, bool* should_compact,
  bool* should_start_over) {
  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(false /* don't consult_young */)) {
    //如果年轻代GC时promote失败
    assert(!_cmsGen->incremental_collection_failed(),
           "Should have been noticed, reacted to and cleared");
    _cmsGen->set_incremental_collection_failed();
  }
  //UseCMSCompactAtFullCollection表示在Full GC时是否执行压缩,默认为true
  //CMSFullGCsBeforeCompaction表示一个阈值,Full GC的次数超过该值才会执行压缩,默认是0
  *should_compact =
    UseCMSCompactAtFullCollection &&
    ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||
     GCCause::is_user_requested_gc(gch->gc_cause()) || //用户通过System.gc方法请求GC
     gch->incremental_collection_will_fail(true /* consult_young */)); //老年代空间不足以执行promote
  *should_start_over = false;
  //如果should_compact为false且clear_all_soft_refs为true
  if (clear_all_soft_refs && !*should_compact) {
    //当clear_all_soft_refs为true时是否需要压缩,默认为true
    if (CMSCompactWhenClearAllSoftRefs) {
      *should_compact = true;
    } else {
      //如果当前GC已经过FinalMarking环节了,在该环节才处理所有的Refenrence,则需要重新开始一轮GC,
      //重新查找待处理的Refenrence
      if (_collectorState > FinalMarking) {
        //将GC的状态设置为重置
        _collectorState = Resetting; // skip to reset to start new cycle
        //执行重置,执行reset方法后_collectorState会被置为Idling
        reset(false /* == !asynch */);
        *should_start_over = true;
      } 
    }
  }
}
 
inline static bool is_user_requested_gc(GCCause::Cause cause) {
    return (cause == GCCause::_java_lang_system_gc ||
            cause == GCCause::_jvmti_force_gc);
  }
 
bool incremental_collection_will_fail(bool consult_young) {
    assert(heap()->collector_policy()->is_two_generation_policy(),
           "the following definition may not be suitable for an n(>2)-generation system");
    //如果consult_young为false,则只考虑incremental_collection_failed,如果promote失败会将其置为true
    //如果为true,collection_attempt_is_safe方法返回执行promote老年代空间是否充足
    return incremental_collection_failed() ||
           (consult_young && !get_gen(0)->collection_attempt_is_safe());
  }

其中_full_gcs_since_conc_gc的调用链如下,CMSCollector::collect方法将其加1,CMSCollector::collect_in_background方法在执行完sweep后将其置为0,其他方法都是读取该属性的值,因此该参数实际表示自上一次后台GC执行完后触发的前台GC的次数。


image.png

补充说明如下:

  1. 是否处于JNI关键区这个只是一个校验而已,正常情况不会进入此逻辑,因为GenCollectedHeap::do_collection方法在在调用各Generation的collect方法前就会检查是否有线程处于JNI关键区,如果有就会通知GC_locker需要GC,并返回, GC_locker会阻塞新的线程进入JNI关键区,直到最后一个线程从JNI关键区退出并触发GC;如果没有,因为执行前台GC是在安全点,STW的状态下,所以不可能有新的线程进入到JNI关键区,即GenCollectedHeap::do_collection判断没有,进入collect方法肯定没有。
    释放bitmapLock、freeListLock锁和CMS Token的目的是为了让正在等待上述锁的CMS Thread恢复正常执行,在执行完某个步骤后检测到前台GC被激活了,就会自动终止执行,将剩余的GC步骤转交给前台GC完成。
  2. 如果是执行堆内存压缩,则需要清空Reference链表,因为里面可能保存了后台GC引用遍历时找到的Reference实例,堆内存压缩会执行自己的引用遍历逻辑来查找Reference实例,为了避免受后台GC的影响,将其清空。
  3. 对于正常的前台GC而言,重新开始就是从InitialMarking开始执行,否则就是继承后台GC,从后台GC已经执行完的步骤的下一步开始执行;对于堆内存压缩,则必须是重新开始,其引用遍历和打标的逻辑跟正常GC不同,即只要需要压缩则执行堆内存压缩,无论是否重新开始。

2.4 do_mark_sweep_work
do_mark_sweep_work有可能是继续后台GC执行剩余的步骤,因此需要根据当前的collectorState做适当的调整,逻辑如下:

void CMSCollector::do_mark_sweep_work(bool clear_all_soft_refs,
  CollectorState first_state, bool should_start_over) {
 
  switch (_collectorState) {
    case Idling:
      //first_state是进入到前台GC时的collectorState,如果其等于Idling说明后台GC未执行
      //should_start_over为true时,会通过reset方法将collectorState置为Idling
      //这两种情形都需要从InitialMarking开始执行
      if (first_state == Idling || should_start_over) {
        _collectorState = InitialMarking;
      }
      break;
    //Precleaning是后台GC特有的步骤,前台GC直接跳过该步骤到FinalMarking步骤
    //AbortablePreclean也是后台GC特有的,但是其下一步就是FinalMarking,所以此处不需要特殊处理
    case Precleaning:
      _collectorState = FinalMarking;
  }
  //执行正常的GC步骤
  collect_in_foreground(clear_all_soft_refs, GenCollectedHeap::heap()->gc_cause());
}

正常的前台GC主要流程如下:


image.png

其中判断是否需要类的卸载的实现如下,在默认配置下_should_unload_classes肯定为true。

void CMSCollector::update_should_unload_classes() {
  _should_unload_classes = false;
  //ExplicitGCInvokesConcurrentAndUnloadsClasses表示通过System.gc方法触发GC时是否卸载Class,默认为false
  if (_full_gc_requested && ExplicitGCInvokesConcurrentAndUnloadsClasses) {
    _should_unload_classes = true;
  } else if (CMSClassUnloadingEnabled) { // CMSClassUnloadingEnabled表示CMS GC时是否允许Class卸载,默认值为true
    //CMSClassUnloadingMaxInterval表示一个阈值,如果自上一次Class卸载后GC的次数超过该值则执行Class卸载,默认值是0
    _should_unload_classes = (concurrent_cycles_since_last_unload() >=
                              CMSClassUnloadingMaxInterval)
                           || _cmsGen->is_too_full(); //老年代的内存使用率太高
  }
}

其中_concurrent_cycles_since_last_unload的调用链如下,do_compaction_work方法将其置为0,sweepWork方法中如果should_unload_classes为true,则将其置为0,否则加1,因此该属性表示自上一次类卸载以来的GC次数。

image.png

enable_discovery方法将_discovering_refs置为true,表示允许Reference实例查找,与之对应的disable_discovery方法的调用链如下:

image.png

即在开始处理各类型的Reference实例链表前就会调用disable_discovery方法,禁止Reference实例查找。负责查找Reference实例的discover_reference方法会检查这个属性是否为true,如果为false则直接返回。

前台GC的Resizing步骤是空实现,直接将状态流转成Resetting,因为在上层GenCollectedHeap::do_collection方法中会在collection之后调用compute_new_size方法,该方法就是Resizing步骤的核心方法。如果前台GC继承后台GC时,GC的状态是Precleaning或者AbortablePreclean时,自动流转至FinalMarking处理。其余各步骤的实现总结如下:

  1. InitialMarking:找到年轻代对象引用的老年代对象和已经被promote到老年代的根节点对象,将其对象地址在markBitMap中打标。
  2. Marking:遍历markBitMap打标的位对应的对象,以其作为根节点,不断遍历其引用的所有对象,如果该对象属于老年代且未在markBitMap中打标,则打标,然后以同样的方式处理其引用的所有对象,直到所有引用的对象都遍历过一遍。
  3. FinalMarking:处理Reference链表中的Reference实例,如果referent对象是存活的,则将其作为根节点不断遍历其所引用的对象,并将该Reference实例从链表中移除;如果如果referent对象不是存活的但是子类需要利用其执行资源清理类动作,同样将其作为根节点不断遍历其所引用的对象,但是依然将其放在Reference链表中。除此之外,如果需要卸载类,FinalMarking还要清理CodeCache中表示编译方法的nmethod,Klass,符号表SymbolTable,字符串表StringTable中的没有关联存活oop的资源。
  4. Sweeping:会按照一定的策略将空间内存块和需要回收的垃圾对象对应的内存块合并成一个大的内存块,并归还到CMS Space中负责管理空闲内存块的FreeList或者Dictionary中,下一次内存分配时可以重新利用这些被归还的空闲内存块。另外,如果卸载类,还要将should_purge置为true,下一次进入安全点后判断该属性为true,就会回收元空间中空闲的多余的内存块了,将其归还给操作系统。
    Resetting:将markBitMap一次性清空
  5. 注意上述步骤都是在一个while循环中执行的,每执行完一个步骤,就会修改当前状态至下一个状态并进入下一次while循环,在while循环中会switch当前状态,并流转至对应状态下的处理逻辑,直到Resetting执行完成,将状态置为Idling,while循环退出。

2.5 do_compaction_work
do_compaction_work负责执行堆内存压缩,因为堆内存压缩是借助对象头而非markBitMap来标记对象的存活,而且堆内存压缩不止处理老年代,年轻代也会处理,所以不能继承后台GC,必须从头开始执行自己的遍历打标逻辑,其主要处理流程如下:


image.png

mark_sweep的4个步骤总结如下

  1. mark_sweep_phase1:遍历所有的根节点,如果其对象头未打标则打标,接着遍历加载该对象对应类的ClassLoader实例和其所引用的其他对象,同样的如果他们未打标则打标,并且接着遍历其ClassLoader实例和其所引用的其他对象,如此循环。除此之外还需要以同样的方式来处理Reference链表中需要保留下来的referent对象,将他们作为根节点,最后清理SystemDictionary,CodeCache,Klass中不再使用的资源,即完成不再使用的类的相关资源卸载。
  2. mark_sweep_phase2:先遍历老年代中的被打标对象,计算该对象的复制地址并将其写入对象头中,然后遍历年轻代的eden区和from区,计算对象复制地址时先复制到老年代,老年代空间不足再复制到eden区,eden区空间不足再复制到from区
  3. mark_sweep_phase3:将根节点oop,JNI弱引用oop,Reference链表头oop,老年代和年轻代的存活对象持有的对象引用oop都遍历一遍,如果其指向的对象的对象头中包含复制地址,则修改oop*让其指向新地址
  4. mark_sweep_phase4:遍历老年代和年轻代的存活对象,将其内存数据复制到对象头中包含的复制地址上,并重新初始化对象头

2.6 gc_epilogue
gc_epilogue的处理跟gc_prologue基本是相反的,其主要流程如下:

image.png

2.7 shouldConcurrentCollect
CMS Thread会不断的休眠,一次最长2s,当被唤醒后会调用shouldConcurrentCollect方法来判断是否需要触发后台GC,如果该方法返回true,则通过collect_in_background方法来执行后台GC。shouldConcurrentCollect方法的实现如下:

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;
  }
  
  //获取FreelistLock锁
  FreelistLocker x(this);
 
  
  //UseCMSInitiatingOccupancyOnly表示是否只使用InitiatingOccupancy作为判断是否GC的标准,默认为false
  if (!UseCMSInitiatingOccupancyOnly) {
    //如果CMSStas的数据是有效的
    if (stats().valid()) {
      //如果当前时间与上一次垃圾回收的时间间隔超过了历史上的时间间隔
      if (stats().time_until_cms_start() == 0.0) {
        return true;
      }
    } else {
      //判断使用率是否超过设定的值
      if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
        if (Verbose && PrintGCDetails) {
          gclog_or_tty->print_cr(
            " CMSCollector: collect for bootstrapping statistics:"
            " occupancy = %f, boot occupancy = %f", _cmsGen->occupancy(),
            _bootstrap_occupancy);
        }
        return true;
      }
    }
  }
 
  //cmsGen认为应该GC
  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");
  //如果年轻代因为老年代空间不足promote失败       
  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;
  }
  //如果元空间需要GC
  if (MetaspaceGC::should_concurrent_collect()) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print("CMSCollector: collect for metadata allocation ");
    }
    return true;
  }
 
  // CMSTriggerInterval表示CMS触发的间隔时间,默认值是-1
  if (CMSTriggerInterval >= 0) {
    if (CMSTriggerInterval == 0) {
      //如果等于0,表示每次都触发
      return true;
    }
 
    //否则检测当前时间与上一次GC的时间间隔是否超过限制
    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;
}
 
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默认为false,表示是否只使用内存使用率作为触发后台GC的条件
  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;
  }
  if (_cmsSpace->should_concurrent_collect()) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because cmsSpace says so ",
        short_name());
    }
    return true;
  }
  return false;
}
 
bool CompactibleFreeListSpace::should_concurrent_collect() const {
  //默认配置下adaptive_freelists为true,所以此方法返回false
  return !adaptive_freelists() && linearAllocationWouldFail();
}

其中_full_gc_requested属性默认为false,其调用链如下:

image.png

其中只有request_full_gc方法将其置为true,collect_in_background方法将其置为false,其他几个都是读取该属性,VM_GenCollectFullConcurrent构造方法的调用链如下:


image.png

即通常情况下最后一个从JNI关键区退出的线程或者System.gc()触发GC时会让 _full_gc_requested属性置为true。

其中与MetaspaceGC::should_concurrent_collect方法相对的set_should_concurrent_collect方法的调用链如下:


image.png

VM_CollectForMetadataAllocation将其置为true,collect_in_background将其置为false,VM_CollectForMetadataAllocation的调用链如下:


image.png

即元空间内存分配失败时会将其置为true,从而触发老年代的后台GC。

2.8 collect_in_background
collect_in_background的主流程如下:

image.png

补充说明如下:

  1. 每次开始执行一个新步骤前都需要检查前台GC是否激活,即上述流程图中第二个判断前台GC是否激活流程实际会执行最多6次,如果已经激活则需要将_foregroundGCShouldWait置为false,释放CMSToken,并唤醒在CGC_lock上等待的VMThread,然后在 CGC_lock上不断循环等待直到VMThread将前台GC执行完成,将_foregroundGCIsActive变成false,最后唤醒CMSThread。
  2. InitialMarking和FinalMarking两步比较特殊,都是通过创建一个VM_CMS_Operation,然后交给VMThread执行,即无论前台GC还是后台GC,执行这两步的时候都需要在安全点,STW的状态下执行。在创建VM_CMS_Operation前,会获取CGC_lock锁,并临时的将_foregroundGCShouldWait置为false,并且如果前台GC被激活了则唤醒在CGC_lock锁上等待的VMThread,VMThread判断此时_foregroundGCShouldWait为false,就会开始执行自己的前台GC逻辑,等前台GC执行完了,由CMSThread后提交的VM_CMS_Operation才会被VMThread执行,执行时会检查GC的状态是否是要求的状态,如果不是说明GC已经执行完了会退出。如果将_foregroundGCShouldWait置为false时前台GC未激活,则VM_CMS_Operation提交给VMThread执行后,VMThread就会执行对应操作,期间不需要检查前台GC是否激活,因为已经STW了不可能再触发前台GC了,直到VMThread将VM_CMS_Operation自行完从安全点退出。
  3. 后台GC跟前台GC相比主要有两大区别:第一,后台GC获取必要的锁后在不使用的前提下会尽可能早的释放掉,且只是获取当前处理步骤所需要的锁;第二,后台GC增加了Precleaning和AbortablePreclean两个步骤,并且在FinalMarking时增加了二次遍历打标的逻辑,前面是为了尽可能缩短FinalMarking二次遍历打标的时间,因为FinalMarking是STW的,后者是必须的,因为从InitialMarking到FinalMarking不是出于STW状态,引用关系会发生变更。其余步骤的底层实现是一样的,调用的是同一个方法。
  4. Precleaning和AbortablePreclean的底层实现都是preclean_work,前者只调用了一次,预清理Reference实例,后者是循环调用了多次,每次都清理survivor区的对象;除此之外,每次调用preclean_work都需要清理modUnionTable中被打标位对应的对象,清理加载不久未promote到老年代的Klass java_mirror属性,即对应类class实例,清理老年代的脏的卡表项对应内存区域的对象。
    AbortablePreclean循环多次调用preclean_work的实现如下,有诸多条件会终止遍历:
void CMSCollector::abortable_preclean() {
  //校验调用线程是否正确
  check_correct_thread_executing();
  //校验CMSPrecleaningEnabled为true,该配置项默认为true
  assert(CMSPrecleaningEnabled,  "Inconsistent control state");
  assert(_collectorState == AbortablePreclean, "Inconsistent control state");
 
  //如果eden区已使用内存大于CMSScheduleRemarkEdenSizeThreshold,该属性默认为2M
  if (get_eden_used() > CMSScheduleRemarkEdenSizeThreshold) {
    TraceCPUTime tcpu(PrintGCDetails, true, gclog_or_tty);
    CMSPhaseAccounting pa(this, "abortable-preclean", _gc_tracer_cm->gc_id(), !PrintGCDetails);
    
    size_t loops = 0, workdone = 0, cumworkdone = 0, waited = 0;
    //不断循环直到这两个条件有一个为true
    while (!(should_abort_preclean() ||
             ConcurrentMarkSweepThread::should_terminate())) {
      //CMSPrecleanRefLists2表示是否清理Reference实例,默认为false
      //CMSPrecleanSurvivors2表示是否清理Survivor区,默认为true
      //preclean_work返回累积清理的卡表项的个数      
      workdone = preclean_work(CMSPrecleanRefLists2, CMSPrecleanSurvivors2);
      cumworkdone += workdone;
      loops++;
      //CMSMaxAbortablePrecleanLoops表示循环的最大次数,默认为0
      if ((CMSMaxAbortablePrecleanLoops != 0) &&
          loops >= CMSMaxAbortablePrecleanLoops) {
        if (PrintGCDetails) {
          gclog_or_tty->print(" CMS: abort preclean due to loops ");
        }
        break;
      }
      //CMSMaxAbortablePrecleanTime表示AbortablePreclean的最大时间,默认是5000,单位毫秒
      if (pa.wallclock_millis() > CMSMaxAbortablePrecleanTime) {
        if (PrintGCDetails) {
          gclog_or_tty->print(" CMS: abort preclean due to time ");
        }
        break;
      }
      //CMSAbortablePrecleanMinWorkPerIteration默认是100,如果清理的脏的卡表数小于该值则应该等待
      if (workdone < CMSAbortablePrecleanMinWorkPerIteration) {
        stopTimer();
         //在CGC_lock上等待最多CMSAbortablePrecleanWaitMillis毫秒,该属性默认值是100
        cmsThread()->wait_on_cms_lock(CMSAbortablePrecleanWaitMillis);
        startTimer();
        waited++;
      }
    }
    if (PrintCMSStatistics > 0) {
      gclog_or_tty->print(" [%d iterations, %d waits, %d cards)] ",
                          loops, waited, cumworkdone);
    }
  }
  //退出循环,获取CMS Token,将状态置为FinalMarking
  CMSTokenSync x(true); // is cms thread
  if (_collectorState != Idling) {
    assert(_collectorState == AbortablePreclean,
           "Spontaneous state transition?");
    _collectorState = FinalMarking;
  } // Else, a foreground collection completed this CMS cycle.
  return;
}

其中should_abort_preclean方法的实现如下:


image.png

_abort_preclean初始为false,其调用链如下:

image.png

preclean方法将其置为false,sample_eden方法中如果eden区的内存使用率超过CMSScheduleRemarkEdenPenetration,该属性的默认值是50则将其置为true,如下:

image.png

但是当_start_sampling为false时sample_eden会直接返回,该属性的调用链如下:

image.png

preclean方法将其置为true,其实现如下:

image.png

其中CMSScheduleRemarkSamplingRatio的默认值是5,CMSScheduleRemarkEdenPenetration的默认值是50,即默认情况下eden区的内存使用率低于10%的时候_start_sampling为true,然后大于50的时候_abort_preclean置为true,从而终止预处理;如果一开始eden区的内存使用率高于10%,则只能等到其他条件触发终止预处理。

相关文章

网友评论

      本文标题:CMS算法实现 - 2

      本文链接:https://www.haomeiwen.com/subject/vtdhhdtx.html