Hotspot CMS算法實現總結 (二)

    目錄

一、Young GC

1、should_collect

2、gc_prologue

3、collect 

4、gc_epilogue

二、Old GC

2.1 should_collect

2.2 gc_prologue

 2.3 collect 

2.4 do_mark_sweep_work

2.5 do_compaction_work

2.6 gc_epilogue

2.7 shouldConcurrentCollect

2.8 collect_in_background


本篇博客繼續上一篇《Hotspot CMS算法實現總結 (一)》,總結下年輕代和老年代的GC主流程。所有Generation的GC都有四個關鍵方法,should_collect決定是否應該GC,gc_prologue執行GC前的預處理動作,collect執行具體的GC,gc_epilogue執行GC完成後的處理動作,下面來詳細說明各Generation的這4個方法的實現。

一、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,讓兩者保持一致,其實現如下:

_soft_end主要用於內存分配場景,內存分配時不能超過該限制,其調用鏈如下:

該屬性主要是給CMS的增量收集模式使用的,增量收集完成會重置這個屬性,即允許在已經完成收集的內存區域中分配對象。增量收集模式通過參數CMSIncrementalMode控制,默認爲false,主要適用於單CPU下的GC,避免長期GC影響了業務線程的正常執行,參考set_soft_end方法的調用鏈,如下:

其他的調用都是直接置爲end。 

3、collect 

     DefNewGeneration::collect方法比較長比較複雜,不貼源碼了,其主要處理流程如下:

其中判斷老年代空間是否充足的實現如下:

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
}

相關補充說明如下:

  1. 如果to區非空,則認爲老年代沒有充足空間執行promote,因爲to區正常情況下都是空的,只有在因爲老年代空間不足導致promote失敗時纔會非空。
  2. 根節點遍歷時會判斷目標oop是否是年輕代的,如果是,則根據分代年齡將其拷貝到to區或者老年代,to區內存不足時會拷貝到老年代中,如果是拷貝到to區,會在對象複製完成後增加複製對象的分代年齡,最後讓目標oop指向新的對象複製地址。注意拷貝到老年代的分代年齡的閾值是根據各年齡的對象總大小和to區的容量動態調整的,最大值是15。
  3. 第二步遍歷所有ClassLoaderData加載的Klass時,只遍歷has_modified_oops返回true的Klass,即Klass對應的類class實例還在年輕代未promote到老年代的Klass
  4. 注意出現promote失敗並不會終止遍歷,此時返回的地址還是原來的對象地址,因此引用類型屬性實際還是指向原來的對象,另外會把promote失敗的oop作爲根節點,以跟第二步同樣的方式不斷遍歷其所有的引用類型屬性。
  5. 遍歷老年代髒的卡表項實際是遍歷其對應內存區域中的對象,注意遍歷前會先將髒的卡表項置爲clean,如果該對象是存活的且不是promoted對象,則遍歷該對象的同樣在髒的卡表項對應的內存區域中的引用類型屬性,即老年代存活對象所引用的年輕代的oop,會將該oop同樣拷貝到to區或者老年代,讓該引用類型屬性指向對象的新複製地址,最後將該oop對應的卡表項置爲youngergen_card。如果引用類型屬性不在髒的內存區域中,說明該屬性未發生變更,不需要處理。
  6. 遍歷所有promote到老年代的對象oop時,會恢復其對象頭,並遍歷其包含的所有引用類型屬性,其引用類型屬性指向的oop如果是年輕代的則同樣將其拷貝到to區或者老年代,讓該引用類型屬性指向對象的新複製地址,最後將該oop對應的卡表項置爲youngergen_card。
  7. 只有第三步和第四步遍歷引用類型屬性時纔會判斷該oop是否是Reference實例,如果是則加入對應類型的待處理列表中。處理時會將referent對象還是存活的Reference實例從鏈表中移除,如果referent對象不是存活的但是需要保留,則將其作爲根節點處理。
  8. promote成功時交換to區和from區是爲了保證to區是空的,在老年代執行壓縮GC時會從to區分配空間;promote失敗時交換to區和from區是爲了保證from區是空的,從而提供一定的內存空間繼續創建對象
  9. 通知GCH promote失敗後會觸發CMSThread執行老年代的後臺GC
  10. 遍歷eden區和from區的對象,恢復其對象頭,去掉可能包含的對象複製地址,注意此時已經成功promote的對象,指向該對象的引用類型屬性都已經成功指向新地址了,後續對象狀態變更都是變更復制對象了,此時還留在eden區和from區的對象因爲不好單獨刪除,所以暫且保留。而promote失敗的對象,指向該對象的引用類型屬性還是指向原來在eden區和from區的對象,這一步恢復對象頭主要是爲了讓promoted失敗的對象可以被正常訪問。
  11. 爲啥遍歷位於年輕代的根節點時不遍歷其所引用的對象了?答案是年輕代的對象在創建時其引用類型屬性肯定是null,設置屬性時要麼是new一個新對象,要麼是使用老年代的對象,如果是前者,new的一個新對象肯定同樣在根節點中,即位於年輕代的根節點,其所引用的年輕代對象也是在根節點oop中

4、gc_epilogue

     其處理流程如下:

補充說明如下:

  1. 正常情況下只從eden區分配老年代對象,如果eden區內存不足了會觸發年輕代GC,如果此時老年代空間不足promote失敗,設置了從from區分配對象標識,則會從from區分配對象,儘可能的提高內存使用率
  2. ChunkPool的內存並不屬於年輕代,這個是JVM分配一些需要經常創建和銷燬的C++類使用的,只是在此處觸發清理邏輯,從而釋放空閒內存,其使用場景可以參考ChunkPool::allocate方法的調用鏈,部分截圖如下:

 

二、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定義,如下:

其中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

其主要處理流程如下:

其中與set_accumulate_modified_oops方法對應的accumulate_modified_oops方法的調用鏈如下:

即只在年輕代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方法的調用鏈如下:

前面兩個是KlassRemSet使用的,第一個用於清除所有Klass的accumulated_modified_oops標識,後者用於判斷是否有未清除的Klass。最後兩個分別是preclean和finalmark步驟使用的,如果有這個標識會將klass的java_mirror作爲根節點遍歷其所有引用類型屬性,以PrecleanKlassClosure的實現爲例,如下:

 另外set_accumulate_modified_oops的調用鏈如下,在gc_epilogue中將其置爲false,在gc_prologue中將其置爲true。

 

補充說明如下:

  • freeListLock實際是底層CMS Space的鎖,從CMS Space分配內存, CMS Space擴容或者對象遍歷時都需要獲取該鎖,參考其調用鏈,如下:

  • bitMapLock就是操作用於記錄對象存活狀態的CMSBitMap的鎖,注意CMSBitMap只在GC時使用,正常情況下就是空的,獲取bitMapLock鎖也都是GC的相關方法,其調用鏈如下:

  • 處於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 

其主要流程如下:

其中判斷是否應該壓縮堆內存,是否重新開始的方法實現如下:

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的次數。 

補充說明如下:

  1. 是否處於JNI關鍵區這個只是一個校驗而已,正常情況不會進入此邏輯,因爲GenCollectedHeap::do_collection方法在在調用各Generation的collect方法前就會檢查是否有線程處於JNI關鍵區,如果有就會通知GC_locker需要GC,並返回, GC_locker會阻塞新的線程進入JNI關鍵區,直到最後一個線程從JNI關鍵區退出並觸發GC;如果沒有,因爲執行前臺GC是在安全點,STW的狀態下,所以不可能有新的線程進入到JNI關鍵區,即GenCollectedHeap::do_collection判斷沒有,進入collect方法肯定沒有。
  2. 釋放bitmapLock、freeListLock鎖和CMS Token的目的是爲了讓正在等待上述鎖的CMS Thread恢復正常執行,在執行完某個步驟後檢測到前臺GC被激活了,就會自動終止執行,將剩餘的GC步驟轉交給前臺GC完成。
  3. 如果是執行堆內存壓縮,則需要清空Reference鏈表,因爲裏面可能保存了後臺GC引用遍歷時找到的Reference實例,堆內存壓縮會執行自己的引用遍歷邏輯來查找Reference實例,爲了避免受後臺GC的影響,將其清空。
  4. 對於正常的前臺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主要流程如下:

其中判斷是否需要類的卸載的實現如下,在默認配置下_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次數。 

 

enable_discovery方法將_discovering_refs置爲true,表示允許Reference實例查找,與之對應的disable_discovery方法的調用鏈如下:

即在開始處理各類型的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,就會回收元空間中空閒的多餘的內存塊了,將其歸還給操作系統。
  5. Resetting:將markBitMap一次性清空

注意上述步驟都是在一個while循環中執行的,每執行完一個步驟,就會修改當前狀態至下一個狀態並進入下一次while循環,在while循環中會switch當前狀態,並流轉至對應狀態下的處理邏輯,直到Resetting執行完成,將狀態置爲Idling,while循環退出。

2.5 do_compaction_work

     do_compaction_work負責執行堆內存壓縮,因爲堆內存壓縮是藉助對象頭而非markBitMap來標記對象的存活,而且堆內存壓縮不止處理老年代,年輕代也會處理,所以不能繼承後臺GC,必須從頭開始執行自己的遍歷打標邏輯,其主要處理流程如下:

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基本是相反的,其主要流程如下:

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,其調用鏈如下:

其中只有request_full_gc方法將其置爲true,collect_in_background方法將其置爲false,其他幾個都是讀取該屬性,VM_GenCollectFullConcurrent構造方法的調用鏈如下:

即通常情況下最後一個從JNI關鍵區退出的線程或者System.gc()觸發GC時會讓 _full_gc_requested屬性置爲true。

其中與MetaspaceGC::should_concurrent_collect方法相對的set_should_concurrent_collect方法的調用鏈如下: 

VM_CollectForMetadataAllocation將其置爲true,collect_in_background將其置爲false,VM_CollectForMetadataAllocation的調用鏈如下:

 即元空間內存分配失敗時會將其置爲true,從而觸發老年代的後臺GC。

2.8 collect_in_background

    collect_in_background的主流程如下:

補充說明如下:

  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方法的實現如下:

_abort_preclean初始爲false,其調用鏈如下:

preclean方法將其置爲false,sample_eden方法中如果eden區的內存使用率超過CMSScheduleRemarkEdenPenetration,該屬性的默認值是50則將其置爲true,如下:

但是當_start_sampling爲false時sample_eden會直接返回,該屬性的調用鏈如下:

preclean方法將其置爲true,其實現如下:

 其中CMSScheduleRemarkSamplingRatio的默認值是5,CMSScheduleRemarkEdenPenetration的默認值是50,即默認情況下eden區的內存使用率低於10%的時候_start_sampling爲true,然後大於50的時候_abort_preclean置爲true,從而終止預處理;如果一開始eden區的內存使用率高於10%,則只能等到其他條件觸發終止預處理。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章