[Inside HotSpot] Serial垃圾回收器Full GC

kelthuzadx發表於2019-05-21

0. Serial垃圾回收器Full GC

Serial垃圾回收器的Full GC使用標記-壓縮(Mark-Compact)進行垃圾回收,該演算法基於Donald E. Knuth提出的Lisp2演算法,它會把所有存活物件滑動到空間的一端,所以也叫sliding compact。Full GC始於gc/serial/tenuredGeneration的TenuredGeneration::collect,它會在GC前後記錄一些日誌,真正的標記壓縮演算法發生在GenMarkSweep::invoke_at_safepoint,我們可以使用-Xlog:gc*得到該演算法的流程:

 Heap address: 0x00000000f9c00000, size: 100 MB, Compressed Oops mode: 32-bit
 GC(0) Pause Young (Allocation Failure)
 GC(1) Pause Full (Allocation Failure)
 GC(1) Phase 1: Mark live objects
 GC(1) Phase 1: Mark live objects 1.136ms
 GC(1) Phase 2: Compute new object addresses
 GC(1) Phase 2: Compute new object addresses 0.170ms
 GC(1) Phase 3: Adjust pointers
 GC(1) Phase 3: Adjust pointers 0.435ms
 GC(1) Phase 4: Move objects
 GC(1) Phase 4: Move objects 0.208ms
 GC(1) Pause Full (Allocation Failure) 40M->0M(96M) 2.102ms
 GC(0) DefNew: 2621K->0K(36864K)
 GC(0) Tenured: 40960K->795K(61440K)
 GC(0) Metaspace: 562K->562K(1056768K)
 GC(0) Pause Young (Allocation Failure) 42M->0M(96M) 3.711ms
 GC(0) User=0.00s Sys=0.00s Real=0.00s

標記-壓縮分為四個階段(如果是fastdebug版jvm,可以使用-Xlog:gc*=trace得到更為詳細的日誌,不過可能詳細過頭了...),這篇文章將圍繞四個階段展開。

1. 階段1:標記存活物件

第一階段對應GC日誌的

 GC(1) Phase 1: Mark live objects

JVM在process_string_table_roots()process_roots()中會遍歷所有型別的GC Root,然後使用XX::oops_do(root_closure)從該GC Root出發標記所有存活物件。XX表示GC Root型別,root_closure表示標記存活物件的方法(閉包)。GC模組有很多閉包(closure),它們代表的是一段程式碼、一種行為。root_closure就是一個MarkSweep::FollowRootClosure閉包。這個閉包很強大,給它一個物件,就能標記這個物件,迭代標記物件的成員,以及物件所在的棧的所有物件及其成員:

// hotspot\share\gc\serial\markSweep.cpp
void MarkSweep::FollowRootClosure::do_oop(oop* p)       { follow_root(p); }

template <class T> inline void MarkSweep::follow_root(T* p) {
  // 如果引用指向的物件不為空且未標記
  T heap_oop = RawAccess<>::oop_load(p);
  if (!CompressedOops::is_null(heap_oop)) {
    oop obj = CompressedOops::decode_not_null(heap_oop);
    if (!obj->mark_raw()->is_marked()) {
      mark_object(obj);   // 標記物件
      follow_object(obj); // 標記物件的成員 
    }
  }
  follow_stack();       // 標記引用所在棧
}

// 如果物件是陣列物件則標記陣列,否則標記物件的成員
inline void MarkSweep::follow_object(oop obj) {
  if (obj->is_objArray()) {
    MarkSweep::follow_array((objArrayOop)obj);
  } else {
    obj->oop_iterate(&mark_and_push_closure);
  }
}

// 標記引用所在的整個棧
void MarkSweep::follow_stack() {
  do {
    // 如果待標記棧不為空則逐個標記
    while (!_marking_stack.is_empty()) {
      oop obj = _marking_stack.pop();
      follow_object(obj);
    }
    // 如果物件陣列棧不為空則逐個標記
    if (!_objarray_stack.is_empty()) {
      ObjArrayTask task = _objarray_stack.pop();
      follow_array_chunk(objArrayOop(task.obj()), task.index());
    }
  } while (!_marking_stack.is_empty() || !_objarray_stack.is_empty());
}

// 標記陣列的型別的Class和陣列成員,比如String[] p = new String[2]
// 對p標記會同時標記java.lang.Class,p[1],p[2]
inline void MarkSweep::follow_array(objArrayOop array) {
  MarkSweep::follow_klass(array->klass());
  if (array->length() > 0) {
    MarkSweep::push_objarray(array, 0);
  }
}

[Inside HotSpot] Serial垃圾回收器Full GC

[Inside HotSpot] Serial垃圾回收器Full GC

既然走到這裡了不如看看JVM是如何標記物件的:

inline void MarkSweep::mark_object(oop obj) {
  // 獲取物件的mark word
  markOop mark = obj->mark_raw();
  // 設定gc標記
  obj->set_mark_raw(markOopDesc::prototype()->set_marked());
  // 垃圾回收器視情況保留物件的gc標誌
  if (mark->must_be_preserved(obj)) {
    preserve_mark(obj, mark);
  }
}

物件的mark work有32bits或者64bits,取決於CPU架構和UseCompressedOops:

// hotspot\share\oops\markOop.hpp
32 位mark lword:
          hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
          JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
          size:32 ------------------------------------------>| (CMS free block)
          PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)

最後的lock2位有不同含義:
          [ptr             | 00]  locked             ptr指向棧上真正的物件頭
          [header      | 0 | 01]  unlocked           普通物件頭
          [ptr             | 10]  monitor            膨脹鎖
          [ptr             | 11]  marked             GC標記

原來垃圾回收的標記就是對每個物件mark word最後兩位置為11,可是如果最後兩位用於其他用途怎麼辦?比如這個物件的最後兩位表示膨脹鎖,那GC就不能對它進行標記了,所以垃圾回收器還需要視情況在額外區域保留物件的mark word(PreservedMark)。回到之前的話題,GC Root有很多,有的是我們耳熟能詳的,有的則是略微少見:

  • 所有已載入的類(ClassLoaderDataGraph::roots_cld_do)
  • 所有Java執行緒當前棧幀的引用和虛擬機器內部執行緒(Threads::possibly_parallel_oops_do)
  • JVM內部使用的引用(Universe::oopds_doSystemDictionary::oops_do)
  • JNI handles(JNIHandles::oops_do)
  • 所有synchronized鎖住的物件引用(ObjectSynchronizer::oops_do)
  • java.lang.management物件(Management::oops_do)
  • JVMTI匯出(JvmtiExport::oops_do)
  • AOT程式碼的堆(AOTLoader::oops_do)
  • code cache(CodeCache::blobs_do)
  • String常量池(StringTable::oops_do)

它們都包含可進行標記的引用,會視情況進行單執行緒標記或者併發標記,JVM會使用CAS(Atomic::cmpxchg)自旋鎖等待標記任務。如果任務全部完成,即標記執行緒和完成計數相等,就結束阻塞。

當物件標記完成後jvm還會使用ref_processor()->process_discovered_references()對弱引用,軟引用,虛引用,final引用根據他們的Java語義做特殊處理,不過與演算法本身沒有太大關係,有興趣的請自行了解。

2. 階段2:計算物件新地址

就算物件新地址的思想是:從地址空間開始掃描,如果cur_obj指標指向已經GC標記過的物件,則將該物件的新地址設定為compact_top,然後compact_top推進,cur_obj推進,直至cur_obj到達地址空間結束。

[Inside HotSpot] Serial垃圾回收器Full GC

計算新地址虛擬碼如下:

// 掃描堆空間
while(cur_obj<space_end){
  if(cur_obj->is_gc_marked()){
    // 如果cur_Obj當前指向已標記過的物件,就計算新的地址
    int object_size += cur_obj->size();
    cur_obj->new_address = compact_top;
    compact_top = cur_obj;
    cur_obj += object_size;
  }else{
    // 否則快速跳過未標記的連續空間
    while(cur_obj<space_end &&!cur_obj->is_gc_marked()){
      cur_obj += cur_obj->size();
    }
  }
}

有了上面的認識,對應到HotSpot實現也比較簡單了。計算物件新地址的程式碼位於CompactibleSpace::scan_and_forward:

// hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_forward(SpaceType* space, CompactPoint* cp) {
  space->set_compaction_top(space->bottom());

  if (cp->space == NULL) {
    cp->space = cp->gen->first_compaction_space();
    cp->threshold = cp->space->initialize_threshold();
    cp->space->set_compaction_top(cp->space->bottom());
  }
  // compact_top為物件新地址的起始
  HeapWord* compact_top = cp->space->compaction_top(); 
  DeadSpacer dead_spacer(space);
  //最後一個標記物件
  HeapWord*  end_of_live = space->bottom(); 
  // 第一個未標記物件
  HeapWord*  first_dead = NULL;

  const intx interval = PrefetchScanIntervalInBytes;

  // 掃描指標
  HeapWord* cur_obj = space->bottom();
  // 掃描終點
  HeapWord* scan_limit = space->scan_limit();

  // 掃描老年代
  while (cur_obj < scan_limit) {
    // 如果cur_obj指向已標記物件
    if (space->scanned_block_is_obj(cur_obj) && oop(cur_obj)->is_gc_marked()) {
      Prefetch::write(cur_obj, interval);
      size_t size = space->scanned_block_size(cur_obj);
      // 給cur_obj指向的物件設定新地址,並前移compact_top
      compact_top = cp->space->forward(oop(cur_obj), size, cp, compact_top);
      // cur_obj指標前移
      cur_obj += size;
      // 修改最後存活物件指標地址
      end_of_live = cur_obj;
    } else {
      // 如果cur_obj指向未標記物件,則獲取這片(可能連續包含未標記物件的)空間的大小
      HeapWord* end = cur_obj;
      do {
        Prefetch::write(end, interval);
        end += space->scanned_block_size(end);
      } while (end < scan_limit && (!space->scanned_block_is_obj(end) || !oop(end)->is_gc_marked()));

      // 如果需要減少物件移動頻率
      if (cur_obj == compact_top && dead_spacer.insert_deadspace(cur_obj, end)) {
        oop obj = oop(cur_obj);
        compact_top = cp->space->forward(obj, obj->size(), cp, compact_top);
        end_of_live = end;
      } else {
        // 否則跳過未存活物件
        *(HeapWord**)cur_obj = end;
        // 如果first_dead為空則將這片空間設定為第一個未存活物件
        if (first_dead == NULL) {
          first_dead = cur_obj;
        }
      }
      // cur_obj指標快速前移
      cur_obj = end;
    }
  }

  if (first_dead != NULL) {
    space->_first_dead = first_dead;
  } else {
    space->_first_dead = end_of_live;
  }
  cp->space->set_compaction_top(compact_top);
}

[Inside HotSpot] Serial垃圾回收器Full GC

[Inside HotSpot] Serial垃圾回收器Full GC

[Inside HotSpot] Serial垃圾回收器Full GC

如果物件需要移動,cp->space->forward()會將新地址放入物件的mark word裡面。計算物件新地址裡面有個小技巧,比如當掃描到一片未存活物件的時候,它把第一個未存活物件設定為該片區域的結尾,這樣下一次掃描到第一個物件可以直接跳到區域尾,節約時間。

3. 階段3:調整物件指標

第二階段設定了所有物件的新地址,但是沒有改變物件的相對地址和GC Root。比如GC Root指向物件A,B,C,這時候A、B、C都有新地址A',B',C',GC Root應該相應調整為指向A',B',C':

[Inside HotSpot] Serial垃圾回收器Full GC

第三階段就是幹這件事的。還記得第一階段GC Root的標記行為嗎?

JVM在process_string_table_roots()process_roots()中會遍歷所有型別的GC Root,然後使用XX::oops_do(root_closure)從該GC Root出發標記所有存活物件。XX表示GC Root型別,root_closure表示標記存活物件的方法(閉包)。

第三階段和第一階段一樣,只是第一階段傳遞的root_closure表示標記存活物件的閉包(FollowRootClosure),第三階段傳遞的root_closure表示調整物件指標的閉包AdjustPointerClosure

// hotspot\share\gc\serial\markSweep.inline.hpp
inline void AdjustPointerClosure::do_oop(oop* p)       { do_oop_work(p); }
template <typename T>
void AdjustPointerClosure::do_oop_work(T* p)           { MarkSweep::adjust_pointer(p); }

template <class T> inline void MarkSweep::adjust_pointer(T* p) {
  T heap_oop = RawAccess<>::oop_load(p);
  if (!CompressedOops::is_null(heap_oop)) {
    // 從地址p處得到物件
    oop obj = CompressedOops::decode_not_null(heap_oop);
    // 從物件mark word中得到新物件地址
    oop new_obj = oop(obj->mark_raw()->decode_pointer());
    if (new_obj != NULL) {
      // 將地址p處設定為新物件地址
      RawAccess<IS_NOT_NULL>::oop_store(p, new_obj);
    }
  }
}

AdjustPointerClosure閉包會遍歷所有GC Root然後調整物件指標,注意,這裡和第一階段有個重要不同是第一階段傳遞的FollowRootClosure閉包會從GC Root出發標記所有可達物件,但是AdjustPointerClosure閉包只會標記GC Root出發直接可達的物件,

[Inside HotSpot] Serial垃圾回收器Full GC

從物件出發尋找可達其他物件這一步是使用的另一個閉包GenAdjustPointersClosure,它會呼叫CompactibleSpace::scan_and_adjust_pointers遍歷整個堆空間然後調整存活物件的指標:

//hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_adjust_pointers(SpaceType* space) {
  // 掃描指標
  HeapWord* cur_obj = space->bottom();
  // 最後一個標記物件
  HeapWord* const end_of_live = space->_end_of_live;
  // 第一個未標記物件
  HeapWord* const first_dead = space->_first_dead; 
  const intx interval = PrefetchScanIntervalInBytes;

  // 掃描老年代
  while (cur_obj < end_of_live) {
    Prefetch::write(cur_obj, interval);
    // 如果掃描指標指向的物件是存活物件
    if (cur_obj < first_dead || oop(cur_obj)->is_gc_marked()) {
      // 調整該物件指標,調整方法和AdjustPointerClosure所用一樣
      size_t size = MarkSweep::adjust_pointers(oop(cur_obj));
      size = space->adjust_obj_size(size);
      // 指標前移
      cur_obj += size;
    } else {
      // 否則掃描指標指向未存活物件,設定掃描指標為下一個存活物件,加速前移
      cur_obj = *(HeapWord**)cur_obj;
    }
  }
}

4. 階段4:移動物件

第四階段傳遞GenCompactClosure閉包,該閉包負責物件的移動:

[Inside HotSpot] Serial垃圾回收器Full GC

移動的程式碼位於CompactibleSpace::scan_and_compact:

//hotspot\share\gc\shared\space.inline.hpp
template <class SpaceType>
inline void CompactibleSpace::scan_and_compact(SpaceType* space) {
  verify_up_to_first_dead(space);
  // 老年代起始位置
  HeapWord* const bottom = space->bottom();
  // 最後一個標記物件
  HeapWord* const end_of_live = space->_end_of_live;

  // 如果該區域所有物件都存活,或者沒有任何物件,或者沒有任何存活物件
  // 就不需要進行移動
  if (space->_first_dead == end_of_live && (bottom == end_of_live || !oop(bottom)->is_gc_marked())) {
    clear_empty_region(space);
    return;
  }

  const intx scan_interval = PrefetchScanIntervalInBytes;
  const intx copy_interval = PrefetchCopyIntervalInBytes;

  // 設定掃描指標cur_obj為空間底部
  HeapWord* cur_obj = bottom;
  // 跳到第一個存活的物件
  if (space->_first_dead > cur_obj && !oop(cur_obj)->is_gc_marked()) {
    cur_obj = *(HeapWord**)(space->_first_dead);
  }

  // 從空間開始到最後一個存活物件為截止進行掃描
  while (cur_obj < end_of_live) {
    // 如果cur_obj執行的物件未標記
    if (!oop(cur_obj)->is_gc_marked()) {
      // 掃描指標快速移動至下一個存活的物件(死物件的第一個word
      // 存放了下一個存活物件的地址,這樣就可以快速移動)
      cur_obj = *(HeapWord**)cur_obj;
    } else {
      Prefetch::read(cur_obj, scan_interval);

      size_t size = space->obj_size(cur_obj);
      // 獲取物件將要被移動到的新地址
      HeapWord* compaction_top = (HeapWord*)oop(cur_obj)->forwardee();
      Prefetch::write(compaction_top, copy_interval);

      // 移動物件,並初始化物件的mark word
      Copy::aligned_conjoint_words(cur_obj, compaction_top, size);
      oop(compaction_top)->init_mark_raw();

      // 掃描指標前移
      cur_obj += size;
    }
  }

  clear_empty_region(space);
}

相關文章