[Inside HotSpot] Serial垃圾回收器 (二) Minor GC

kelthuzadx發表於2019-05-23

Serial垃圾回收器Minor GC

1. DefNewGeneration垃圾回收

新生代使用複製演算法做垃圾回收,比老年代的標記-壓縮簡單很多,所有回收程式碼都位於DefNewGeneration::collect:

// hotspot\share\gc\serial\defNewGeneration.cpp
void DefNewGeneration::collect(bool   full,
                               bool   clear_all_soft_refs,
                               size_t size,
                               bool   is_tlab) {
  SerialHeap* heap = SerialHeap::heap();
  _old_gen = heap->old_gen();
  // 如果新生代全是存活物件,老年代可能容不下新生代的晉升
  // 則設定增量垃圾回收失敗,直接返回
  if (!collection_attempt_is_safe()) {
    heap->set_incremental_collection_failed(); 
    return;
  }
  ...
  // 各種閉包初始化
  IsAliveClosure is_alive(this);
  ...
  
  {
    // 掃描GC Root,用快速掃描閉包做物件複製
    StrongRootsScope srs(0);
    heap->young_process_roots(&srs,
                              &fsc_with_no_gc_barrier,
                              &fsc_with_gc_barrier,
                              &cld_scan_closure);
  }
  // 用快速成員處理閉包處理非GC Root直達物件
  evacuate_followers.do_void();
  // 特殊處理軟引用,弱引用,虛引用,final引用
 ...

  // 如果晉升成功,則清空eden,from;交換from,to分割槽;調整老年代晉升閾值
  // 同時還需要確保晉升成功的情況下to區一定是空的
  if (!_promotion_failed) {
    eden()->clear(SpaceDecorator::Mangle);
    from()->clear(SpaceDecorator::Mangle);
    if (ZapUnusedHeapArea) {
      to()->mangle_unused_area();
    }
    swap_spaces();
    adjust_desired_tenuring_threshold();
    AdaptiveSizePolicy* size_policy = heap->size_policy();
    size_policy->reset_gc_overhead_limit_count();
  } 
  // 否則晉升失敗,提醒老年代
  else {
    _promo_failure_scan_stack.clear(true); 
    remove_forwarding_pointers();
    log_info(gc, promotion)("Promotion failed");
    swap_spaces();
    from()->set_next_compaction_space(to());
    heap->set_incremental_collection_failed();
    _old_gen->promotion_failure_occurred();
  }
  // 更新gc日誌,清除preserved mar
  ...
}

在做Minor GC之前會檢查此次垃圾回收是否安全(collection_attempt_is_safe),所謂是否安全是指最壞情況下新生代全是需要晉升的存活物件,這時候老年代能否安全容納下。如果JVM回答可以做垃圾回收,那麼再做下面的展開。

2. 快速掃描閉包(FastScanClosure)

新生代的複製動作主要位於young_process_roots(),該函式首先會掃描所有型別的GC Root,使用快速掃描閉包配合GC Root將直達的存活物件複製到To survivor區,然後再掃描從老年代指向新生代的應用。快速掃描閉包指的是FastScanClosure,它的程式碼如下:

// hotspot\share\gc\shared\genOopClosures.inline.hpp
inline void FastScanClosure::do_oop(oop* p)       { FastScanClosure::do_oop_work(p); }
template <class T> inline void FastScanClosure::do_oop_work(T* p) {
  // 從地址p處獲取物件 
  T heap_oop = RawAccess<>::oop_load(p);
  if (!CompressedOops::is_null(heap_oop)) {
    oop obj = CompressedOops::decode_not_null(heap_oop);
    // 如果物件位於新生代
    if ((HeapWord*)obj < _boundary) {
      // 如果物件有轉發指標(相當於已複製過)就保持原位
      // 否則根據情況進行復制
      oop new_obj = obj->is_forwarded() ? obj->forwardee()
                                        : _g->copy_to_survivor_space(obj);
      RawAccess<IS_NOT_NULL>::oop_store(p, new_obj);
      if (is_scanning_a_cld()) {
        do_cld_barrier();
      } else if (_gc_barrier) {
        // 根據情況設定gc barrier
        do_barrier(p);
      }
    }
  }
}

一句話總結,快速掃描閉包的能力是視情況複製地址所指物件或者晉升它。這段程式碼有兩個值得提及的地方:

  1. 根據情況進行復制的copy_to_survivor_space()
  2. 根據情況設定gc屏障的do_barrier()

2.1 新生代到To survivor的複製

先說第一個複製:

// hotspot\share\gc\serial\defNewGeneration.cpp
oop DefNewGeneration::copy_to_survivor_space(oop old) {
  size_t s = old->size();
  oop obj = NULL;
  // 如果物件還年輕就在to區分配空間
  if (old->age() < tenuring_threshold()) {
    obj = (oop) to()->allocate_aligned(s);
  }
  // 如果物件比較老或者to區分配失敗,晉升到老年代
  if (obj == NULL) {
    obj = _old_gen->promote(old, s);
    if (obj == NULL) { // 晉升失敗處理
      handle_promotion_failure(old);
      return old;
    }
  } else {
    // 如果to分配成功,在新分配的空間裡面放入物件
    const intx interval = PrefetchCopyIntervalInBytes;
    Prefetch::write(obj, interval);
    Copy::aligned_disjoint_words((HeapWord*)old, (HeapWord*)obj, s);
    // 物件年齡遞增且加入年齡表
    obj->incr_age();
    age_table()->add(obj, s);
  }
  // 把新地址插入物件mark word,表示該物件已經複製過了。
  old->forward_to(obj);
  return obj;
}

程式碼很清晰,如果GC Root裡面引用的物件年齡沒有超過晉升閾值,就把它從新生代(Eden+From)轉移到To,如果超過閾值直接從新生代轉移到老年代。

2.2 GC屏障

然後說說gc barrier。之前文章提到過老年代(TenuredGeneration,久任代)繼承自卡表代(CardGeneration),卡表代把堆空間劃分為一張張512位元組的卡片,如果某個卡是髒卡(dirty card)就表示該卡表示的512位元組記憶體空間存在指向新生代的物件,就需要掃描這篇區域。do_barrier()會檢查是否開啟gc barrier,是否老年代地址p指向的物件存在指向新生代的物件。如果條件都滿足就會將卡標記為dirty,那麼具體是怎麼做的?

//hotspot\share\gc\shared\cardTableRS.hpp
class CardTableRS: public CardTable {
  ...
  void inline_write_ref_field_gc(void* field, oop new_val) {
    jbyte* byte = byte_for(field);
    *byte = youngergen_card;
  }
}

field表示這個老年代物件的地址,byte_for()會找到該地址對應的card,然後*byte = youngergen_card標記為髒卡,再來看看byte_for()又是怎麼根據地址找到card的:

//hotspot\share\gc\shared\cardTable.hpp
class CardTable: public CHeapObj<mtGC> {
  ...
  jbyte* byte_for(const void* p) const {
    jbyte* result = &_byte_map_base[uintptr_t(p) >> card_shift];
    return result;
  }
}

card_shift表示常量9,卡表是一個位元組陣列,每個位元組對映老年代512位元組,計算方法就是當前地址除以512向下取整,然後查詢卡表陣列對應的位元組:

[Inside HotSpot] Serial垃圾回收器 (二) Minor GC

3. 快速成員處理閉包(FastEvacuateFollowersClosure)

不難看出,快速掃描閉包只是複製和晉升了GC Root直接可達的物件引用。但問題是物件還可能有成員,可達性分析是從GC Root出發尋找物件引用,以及物件成員的引用,物件成員的成員的引用...快速成員處理閉包正是處理剩下不那麼直接的物件引用:

//hotspot\share\gc\serial\defNewGeneration.cpp
void DefNewGeneration::FastEvacuateFollowersClosure::do_void() {
  do {
    // 對整個堆引用快速成員處理閉包,注意快速掃描閉包是不能單獨行動的
    // 他還需要藉助快速掃描閉包的力量,因為快速掃描閉包有複製物件的能力
    // _scan_cur_or_nonheap表示快速掃描閉包
    // _scan_older表示帶gc屏障的快速掃描閉包
    _heap->oop_since_save_marks_iterate(_scan_cur_or_nonheap, _scan_older);
  } while (!_heap->no_allocs_since_save_marks());
}

第一步快速掃描閉包可能會將Eden+From區的物件提升到老年代,這時候如果只處理新生代是不夠的,因為這些提升了的物件可能還有新生代的成員域,所以快速成員處理閉包作用的是除了To survivor的整個堆(Eden+From+Tenured)。

//hotspot\share\gc\shared\space.inline.hpp
template <typename OopClosureType>
void ContiguousSpace::oop_since_save_marks_iterate(OopClosureType* blk) {
  HeapWord* t;
  // 掃描指標為灰色物件開始
  HeapWord* p = saved_mark_word();
  const intx interval = PrefetchScanIntervalInBytes;
  do {
    // 灰色物件結束
    t = top();
    while (p < t) {
      Prefetch::write(p, interval);
      oop m = oop(p);
      // 迭代處理物件m的成員&&返回物件m的大小
      // 掃描指標向前推進
      p += m->oop_iterate_size(blk);
    }
  } while (t < top());
  set_saved_mark_word(p);
}

這裡比較坑的是oop_iterate_size()函式會同時迭代處理物件m的成員並返回物件m的大小...還要注意oop_iterate_size()傳入的blk表示的是快速掃描閉包,同樣一句話總結,快速成員處理閉包的能力是遞迴式處理一個分割槽所有物件及物件成員,這種能力配合上快速掃描閉包最終效果就是將一個分割槽的物件視情況複製到到To survivor區或者晉升到老年代。

關於快速掃描閉包和快速成員處理閉包用圖片說明可能更簡單,假設有ABCD四個物件:

[Inside HotSpot] Serial垃圾回收器 (二) Minor GC

當快速掃描閉包完成時A假設會進入To區域:

[Inside HotSpot] Serial垃圾回收器 (二) Minor GC

當快速成員處理閉包完成時A的成員B和老年代C指向的成員D也會進入To:

[Inside HotSpot] Serial垃圾回收器 (二) Minor GC

相關文章