深入分析 OpenJDK G1 FullGC 原理

語言: CN / TW / HK

導讀

本文主要從程式碼層面介紹 OpenJDK G1(garbage first) FullGC 的工作原理,基於 OpenJDK 從 GC(garbage collection) 入口處開始分析整個 FullGC 原理的核心程式碼與執行過程;並對比 OpenJDK8 與 OpenJDK11 中單執行緒與多執行緒實現 FullGC 的異同。希望幫助大家更深入地理解 G1 FullGC 原理,並瞭解程式碼中可能導致 GC 時間長的物件分配方式。

閱讀本文需要對 JVM、JDK、java物件、物件記憶體分配、垃圾回收等概念有一定了解,沒有 JVM 基礎的同學可參考《深入理解Java虛擬機器》,《The Garbage Collection Handbook》等。本文涉及原始碼可參考openjdk github source code

G1 GC的知識回顧

為了幫助大家更全面地理解 FullGC 的邏輯和前因後果,我們先整體簡單介紹下 G1 GC 。GC 主要指在程式執行過程中,物件不再使用後,由 JVM 提供的程式將物件銷燬並回收記憶體,將該記憶體再加入到可用記憶體中;而 G1GC,是 JVM 提供的一種垃圾回收器,可以通過多 worker 並行回收,並且通過 CMS 併發標記,此外通過 region 的分代回收,可以較好的控制單次回收的暫停時間,並且有效的抑制碎片化的問題。但 G1 並不是完美的回收器,相比於 CMS GC,G1GC CPU 負載相對較高;吞吐量在小記憶體上可能相對較弱,但 G1GC 整體在回收時間和效率上都表現較為優秀,是當前 java 業務中使用較為頻繁的垃圾回收器。但隨著使用場景的變換,為了在業務中更好地完成垃圾回收調優,可以更多地瞭解下這項技術的基礎知識。

  1. G1 將整個 JVM 執行時堆分為若干個 region,region 大小在執行之初設定,且一經設定不可改變;每個 region 都可能被作以下分別:包括 Eden,Survivor,Old,Humongous 和 Free 等區域,其中 Eden region 指年輕代,一般物件(大小小於 region 的一半)物件分配的預設選擇區域,並且為每次記憶體空間不足時的優先回收區;從一次垃圾回收存活下來的物件會被 copy 到 survivor region,經過幾輪迴收後,依然存活的物件太多,則這些物件會被移動到 Old region;當物件大小大於region 一半會被直接分配到 Humongous region,只有在 Full GC 或者 CMS 後才有機會回收。

  2. 回收包含 4 部分 YoungGC(Pause)、MixGC、Concurrent Marking 和 FullGC

    YoungGC:選擇全部年輕代(Eden),根據暫停時間要求決定回收年輕代的數量。

    • stop the world(stw)暫停所有應用執行緒,G1 建立回收集 Collection Set(cset)
    • 掃描 root,從棧、classloader、JNIHandle 等出發,掃描標記直接引用的物件
    • 基於要回收的 region 的 rset 來掃描標記對應 region 的存活物件
    • 然後將活物件 copy 到新 region
    • 處理其他引用

    Concurrent Marking Cycle: JVM 記憶體使用比例超過一定閾值,會觸發全域性併發標記,包括 clean up 階段(這裡會清理出部分大物件)

    MixGC:選擇全部年輕代和部分老年代,會按照垃圾佔比高低來排序,優先回收垃圾多的 set,G1也得名於此(Garbage first),回收過程同 YoungGC 一樣。

    FullGC: MixGC 來不及回收或回收不掉,分配記憶體仍失敗,就會觸發全域性回收,包括所有年輕代、老年代,大物件以及少量其他的 JVM 管理的記憶體,過程總體同 YoungGC 一樣。不過掃描 root 過程會將整個堆完全掃描完。對於有些高流量的業務程式而言,適當的 FullGC 可以更好的調整整個記憶體,但一般情況而言,因為回收暫停時間較長,我們應當儘量避免 FullGC。

  3. G1 FullGC 回收流程圖簡圖如下

G1 FullGC入口(觸發條件)

觸發 FullGC 的條件主要分兩大類,第一類是在物件分配時發現記憶體不足導致分配失敗,會觸發回收邏輯,在 young gc/mix gc 嘗試回收後,都不能釋放足夠記憶體時,便會觸發 FullGC;第二類為業務程式碼中主動呼叫回收記憶體函式,比如 System.gc。兩類最終都會呼叫到 do_collection(),此處不涉及 parallel,OpenJDK8 和 OpenJDK11 入口相同

 // 各種分配失敗時的呼叫入口
G1CollectedHeap::mem_allocate
    VM_G1CollectForAllocation::doit()  
        G1CollectedHeap::satisfy_failed_allocation()
            G1CollectedHeap::do_collection()

 // System.gc()和 metadata回收            
G1CollectedHeap::collect(GCCause::Cause cause)
    VM_G1CollectFull::doit()
        G1CollectedHeap::do_full_collection()
            G1CollectedHeap::do_collection()

G1 FullGC回收前準備——prepare_collection

從 do_collection 調入後,對於 OpenJDK8,以 G1MarkSweep::invoke_at_safepoint 函式為邊界,該函式前為回收準備,之後為收尾工作;而 OpenJDK11 則將整個 prepare_collection、collect 和 complete 三步都用 g1FullCollector 類進行封裝,邏輯更加清晰。

準備過程兩者相似,可簡單分為三類

  • 資料資訊檢查及記錄:包括計時器,trace、safepoint 檢查、日誌列印等資料處理部分
  • concurrent_mark 相關操作和暫停:包括 concurrent_mark 的 abort,cm discovery 禁用等
  • 回收準備:heap 集合整理清除、policy 策略更新、開啟 stw discovery、處理物件偏向鎖等
  // openjdk11
void G1FullCollector::prepare_collection() {
  _heap->g1_policy()->record_full_collection_start();
  _heap->print_heap_before_gc();
  _heap->print_heap_regions();
   // 終止併發標記,禁用cm discovery
  _heap->abort_concurrent_cycle();
  _heap->verify_before_full_collection(scope()->is_explicit_gc());
   // 預處理當前堆
  _heap->gc_prologue(true);
  _heap->prepare_heap_for_full_collection();
  ...
   // 儲存偏向鎖的markword並進行初始標記
  BiasedLocking::preserve_marks();
  clear_and_activate_derived_pointers();
}

void BiasedLocking::preserve_marks() {
  ...
  Thread* cur = Thread::current();
  for (JavaThread* thread = Threads::first(); thread != NULL; thread = thread->next()) {
    if (thread->has_last_Java_frame()) {
      RegisterMap rm(thread);
      for (javaVFrame* vf = thread->last_java_vframe(&rm); vf != NULL; vf = vf->java_sender()) {
        GrowableArray<MonitorInfo*> *monitors = vf->monitors();
        if (monitors != NULL) {
          int len = monitors->length();
          // Walk monitors youngest to oldest
          for (int i = len - 1; i >= 0; i--) {
            MonitorInfo* mon_info = monitors->at(i);
            if (mon_info->owner_is_scalar_replaced()) continue;
            oop owner = mon_info->owner();
            if (owner != NULL) {
              markOop mark = owner->mark();
              if (mark->has_bias_pattern()) {
                _preserved_oop_stack->push(Handle(cur, owner));
                _preserved_mark_stack->push(mark);
              }
            }
          }
        }
      }
    }
  }
}

上述程式碼為垃圾回收前準備過程的封裝,OpenJDK8和OpenJDK11整體過程基本一致(可進入呼叫函式與 OpenJDK8 對照,此處不展開),區別在於 OpenJDK11 會在 g1FullCollector 初始化過程中,根據 parallel 的使用情況對 preserve_stack、oop_stack、arrayOop_stack、compaction_points 等資料結構按照並行的執行緒數進行切分,初始化並用陣列儲存,為 parallel 回收做準備,服務於並行 FullGC。

  // openjdk11
G1FullCollector::G1FullCollector(G1CollectedHeap* heap, GCMemoryManager* memory_manager, bool explicit_gc, bool clear_soft_refs) :
    _heap(heap),
    _scope(memory_manager, explicit_gc, clear_soft_refs),
    _num_workers(calc_active_workers()),
    _oop_queue_set(_num_workers),
    _array_queue_set(_num_workers),
    _preserved_marks_set(true),
    _serial_compaction_point(),
    _is_alive(heap->concurrent_mark()->nextMarkBitMap()),
    _is_alive_mutator(heap->ref_processor_stw(), &_is_alive),
    _always_subject_to_discovery()
{ 
  assert(SafepointSynchronize::is_at_safepoint(), "must be at a safepoint");
  _preserved_marks_set.init(_num_workers);
  _markers = NEW_C_HEAP_ARRAY(G1FullGCMarker*, _num_workers, mtGC);
  _compaction_points = NEW_C_HEAP_ARRAY(G1FullGCCompactionPoint*, _num_workers, mtGC);
  for (uint i = 0; i < _num_workers; i++) {
    _markers[i] = new G1FullGCMarker(i, _preserved_marks_set.get(i), mark_bitmap());
    _compaction_points[i] = new G1FullGCCompactionPoint();
    _oop_queue_set.register_queue(i, marker(i)->oop_stack());
    _array_queue_set.register_queue(i, marker(i)->objarray_stack());
  }
}

G1 FullGC回收核心邏輯——collect

回收核心過程基本分為4步如下,包括標記階段(phase1_mark_live_objects),複製和回收準備階段(phase2_prepare_compaction),調整指標階段(phase3_adjust_pointers)和壓縮清理階段(phase4_do_compaction):

 // openjdk11
void G1FullCollector::collect() {
  phase1_mark_live_objects();

  phase2_prepare_compaction();

  phase3_adjust_pointers();

  phase4_do_compaction();
}

OpenJDK11 中,這4步驟被封裝成 4個 task ,繼承自 AbstractGangTask,都儲存有 monitor 和可執行的 GangWorker,利用 run_task 來根據 worker 數目分多執行緒執行 task,而執行邏輯封裝在 task 的 work 函式中,每個 task 中會傳入 worker_id,操作時根據 worker_id 來利用 G1FullCollector 的資料結構來進行操作。

void G1FullCollector::phase1_mark_live_objects() {
  GCTraceTime tm("Phase 1: Mark live objects", G1Log::fine(), true, scope()->timer(), scope()->tracer()->gc_id());

  // Do the actual marking.
  G1FullGCMarkTask marking_task(this);
  run_task(&marking_task);
...
}

void G1FullCollector::run_task(AbstractGangTask* task) {
...
  _heap->workers()->run_task(task);
...
}

1. phase1_mark_live_objects

以 marker 過程為例,第 5 行根據 worker_id 取到 collector 中準備好的資料結構,通過root_processor 來來執行三個閉包函式,最後會一路呼叫到每個 OOP 的 mark_and_push,對整個堆進行標記,此過程與 OpenJDK8 基本一致。

 // openjdk11
void G1FullGCMarkTask::work(uint worker_id) {
  Ticks start = Ticks::now();
  ResourceMark rm;
  G1FullGCMarker* marker = collector()->marker(worker_id);
  MarkingCodeBlobClosure code_closure(marker->mark_closure(), !CodeBlobToOopClosure::FixRelocations);

  if (ClassUnloading) {
    _root_processor.process_strong_roots(
        marker->mark_closure(),
        marker->cld_closure(),
        &code_closure);
  } else {
    _root_processor.process_all_roots_no_string_table(
        marker->mark_closure(),
        marker->cld_closure(),
        &code_closure);
  }

  // Mark stack is populated, now process and drain it.
  marker->complete_marking(collector()->oop_queue_set(), collector()->array_queue_set(), &_terminator);

  // This is the point where the entire marking should have completed.
  assert(marker->oop_stack()->is_empty(), "Marking should have completed");
  assert(marker->objarray_stack()->is_empty(), "Array marking should have completed");
}

void G1RootProcessor::process_strong_roots(OopClosure* oops,
                                           CLDClosure* clds,
                                           CodeBlobClosure* blobs) {

  process_java_roots(oops, clds, clds, NULL, blobs, NULL, 0);
  process_vm_roots(oops, NULL, NULL, 0);

  _process_strong_tasks.all_tasks_completed();
}

void G1RootProcessor::process_vm_roots(OopClosure* strong_roots,
                                       OopClosure* weak_roots,
                                       G1GCPhaseTimes* phase_times,
                                       uint worker_i) {
  {
    G1GCParPhaseTimesTracker x(phase_times, G1GCPhaseTimes::UniverseRoots, worker_i);
    if (!_process_strong_tasks.is_task_claimed(G1RP_PS_Universe_oops_do)) {
      Universe::oops_do(strong_roots);
    }
  }
  // 各類可能的roots
  ...
}

bool SubTasksDone::is_task_claimed(uint t) {
  assert(0 <= t && t < _n_tasks, "bad task id.");
  uint old = _tasks[t];
  if (old == 0) {
    old = Atomic::cmpxchg(1, &_tasks[t], 0);
  }
  bool res = old != 0;
...
  return res;
}

template <class T> inline void G1FullGCMarker::mark_and_push(T* p) {
  T heap_oop = oopDesc::load_heap_oop(p);
  if (!oopDesc::is_null(heap_oop)) {
    oop obj = oopDesc::decode_heap_oop_not_null(heap_oop);
    if (mark_object(obj)) {
      _oop_stack.push(obj);
      assert(_bitmap->isMarked((HeapWord*) obj), "Must be marked now - map self");
    } else {
      assert(_bitmap->isMarked((HeapWord*) obj), "Must be marked by other");
    }
  }
}

補充多執行緒實現思路

以 mark 過程為例,簡單說明下多執行緒回收思路。

1)首先在 java 程式執行時,可以通過新增 -XX:ParallelGCThreads=8 指定具體回收時的執行緒數;在啟動後,SharedHeap 物件的初始化時,會呼叫 WorkGang::initialize_workers(),初始化 8 個 GC 執行緒(一般是以 GangWorker 實現,繼承自 WorkerThread,也被稱為工人執行緒)。隨後執行緒開始執行會呼叫到 GangWorker 的 run 函式,調到 loop,進入 wait 階段等待喚醒來執行任務,這個迴圈等待會持續整個 java 程式執行過程。

void GangWorker::run() {
  initialize();
  loop();
}

void GangWorker::loop() {
...
  Monitor* gang_monitor = gang()->monitor();
  for ( ; /* !terminate() */; ) {
    WorkData data;
    int part;  // Initialized below.
    {
      MutexLocker ml(gang_monitor);
      gang()->internal_worker_poll(&data);
      for ( ; /* break or return */; ) {
        // Check for new work.
        if ((data.task() != NULL) &&
            (data.sequence_number() != previous_sequence_number)) {
          if (gang()->needs_more_workers()) {
            gang()->internal_note_start();
            gang_monitor->notify_all();
            part = gang()->started_workers() - 1;
            break;
          }
        }
        // Nothing to do.
        gang_monitor->wait(/* no_safepoint_check */ true);
        gang()->internal_worker_poll(&data);
...
        data.task()->work(part);
...
      }
    }
  }
}

2)所有的 XXXTask 繼承自 AbstractGangTask,會實現 work 方法;在回收過程執行時,會將待執行的 task 傳給 heap 的 workers 來執行,即會呼叫到 Sharedheap 中提前儲存好的 FlexibleWorkGang 的 run_task。而 FlexibleWorkGang 繼承自 WorkGang,run_task 方法實現如下,會喚醒所有GC執行緒,並等待所有執行緒執行結束後返回。

// workgroup.cpp
void FlexibleWorkGang::run_task(AbstractGangTask* task) {
  WorkGang::run_task(task, (uint) active_workers());
}

void WorkGang::run_task(AbstractGangTask* task, uint no_of_parallel_workers) {
  task->set_for_termination(no_of_parallel_workers);
  MutexLockerEx ml(monitor(), Mutex::_no_safepoint_check_flag);
  ...
  monitor ()-> notify_all (); 
  // Wait for them to be finished
  while (finished_workers() < no_of_parallel_workers) {
    if (TraceWorkGang) {
      tty->print_cr("Waiting in work gang %s: %d/%d finished sequence %d",
                    name(), finished_workers(), no_of_parallel_workers,
                    _sequence_number);
    }
    monitor()->wait(/* no_safepoint_check */ true);
  }
  ...
}

3)task 開始執行後,會通過 processor 執行器執行到每個 task 的 work 方法,並根據執行執行緒傳入worker_id,比如 G1FullGCMarkTask 的 _root_processor.process_strong_roots 函式,呼叫關係的其中一條分支如下,每一個 workgang 都會通過 ALL_JAVA_THREADS 來順序遍歷所有的 java 執行緒來執行 mark 操作,若已經執行過或正在執行就跳過,保證執行完所有執行緒,包括一個 VMThread,其中 mark 過程傳入的 OopClosure、CLDClosure 為對應 worker_id 的 mark_closure、cld_closure,執行時會呼叫每個 marker 準備好的 oopStack 等資料結構來執行,都執行結束後所有的 GangWorker繼續進入等待任務的過程中,直到下一個 run_task。

// process_strong_roots
// -> process_java_roots
// --> Threads::possibly_parallel_oops_do

void Threads::possibly_parallel_oops_do(OopClosure* f, CLDClosure* cld_f, CodeBlobClosure* cf) {
  SharedHeap* sh = SharedHeap::heap();
  bool is_par = sh->n_par_threads() > 0;
  assert(!is_par ||
         (SharedHeap::heap()->n_par_threads() ==
          SharedHeap::heap()->workers()->active_workers()), "Mismatch");
  int cp = SharedHeap::heap()->strong_roots_parity();
  ALL_JAVA_THREADS(p) {
    if (p->claim_oops_do(is_par, cp)) {
      p->oops_do(f, cld_f, cf);
    }
  }
  VMThread* vmt = VMThread::vm_thread();
  if (vmt->claim_oops_do(is_par, cp)) {
    vmt->oops_do(f, cld_f, cf);
  }
}

2. phase2_prepare_compaction

標記過後,準備壓縮清理,根據標記情況計算地址,詳細邏輯在 G1CalculatePointersClosure 實現如下;根據標記結果,通過 doHeapRegion 遍歷含有存活物件的 region,並將存活物件複製到新的 region 內,copy 同時將物件頭指向舊物件,為調整指標和清理舊 region 做準備。

  // openjdk11
void G1FullGCPrepareTask::work(uint worker_id) {
  ...
  G1FullGCCompactionPoint* compaction_point = collector()->compaction_point(worker_id);
  G1CalculatePointersClosure closure(collector()->mark_bitmap(), compaction_point);
  G1CollectedHeap::heap()->heap_region_par_iterate_from_start(&closure, &_hrclaimer);
  ...
}

void G1CollectedHeap::heap_region_par_iterate_from_start(HeapRegionClosure* cl, HeapRegionClaimer *hrclaimer) const {
  _hrm.par_iterate(cl, hrclaimer, 0);
}

void HeapRegionManager::par_iterate(HeapRegionClosure* blk, HeapRegionClaimer* hrclaimer, const uint start_index) const {
  const uint n_regions = hrclaimer->n_regions();
  for (uint count = 0; count < n_regions; count++) {
    const uint index = (start_index + count) % n_regions;
    ...
    HeapRegion* r = _regions.get_by_index(index);
    if (hrclaimer->is_region_claimed(index)) {
      continue;
    }
    if (!hrclaimer->claim_region(index)) {
      continue;
    }
    bool res = blk->do_heap_region(r);
    if (res) {
      return;
    }
  }
}

bool G1FullGCPrepareTask::G1CalculatePointersClosure::doHeapRegion(HeapRegion* hr) {
  if (hr->isHumongous()) {
    oop obj = oop(hr->humongous_start_region()->bottom());
    if (_bitmap->isMarked((HeapWord*) obj)) {
      if (hr->startsHumongous()) {
        obj->forward_to(obj);
      }
    } else {
      free_humongous_region(hr);
    }
  } else if (!hr->isHumongous()) {
    prepare_for_compaction(hr);
  }
  reset_region_metadata(hr);
  return false;
}

3. phase3_adjust_pointers

此步驟和phase2類似,遍歷存活物件region,通過物件頭記錄的地址,將 live 的物件引用指向重新計算的地址。由 phase2 和 phase3 兩個步驟可見,同樣大小的記憶體佔用,在 GC 過程中,存活物件數量越多會引起更多的複製和調整指標工作,導致更長的總回收時間。

 // openjdk11
void G1FullGCAdjustTask::work(uint worker_id) {
  ...
  // Needs to be last, process_all_roots calls all_tasks_completed(...).
  _root_processor.process_all_roots(
      &_adjust,
      &adjust_cld,
      &adjust_code);
  ...
   // 在此步驟前adjust掉weak,preserved string等等,    // 此步驟adjust pointers region by region
  G1AdjustRegionClosure blk(collector()->mark_bitmap(), worker_id);
  G1CollectedHeap::heap()->heap_region_par_iterate_from_worker_offset(&blk, &_hrclaimer, worker_id);
  log_task("Adjust task", worker_id, start);
}

4. phase4_do_compaction

最後逐個 region 進行處理,若沒有存活物件,清理掉 region;若有存活物件,則將物件按之前計算copy 到新地址,將舊 region 重置。

 // openjdk11
void G1FullGCCompactTask::work(uint worker_id) {
  Ticks start = Ticks::now();
  GrowableArray<HeapRegion*>* compaction_queue = collector()->compaction_point(worker_id)->regions();
  for (GrowableArrayIterator<HeapRegion*> it = compaction_queue->begin();
       it != compaction_queue->end();
       ++it) {
    compact_region(*it);
  }
  G1ResetHumongousClosure hc(collector()->mark_bitmap());
  G1CollectedHeap::heap()->heap_region_par_iterate_from_worker_offset(&hc, &_claimer, worker_id);
  ...
}

void G1FullGCCompactTask::compact_region(HeapRegion* hr) {
  ...
  G1CompactRegionClosure compact(collector()->mark_bitmap());
  hr->apply_to_marked_objects(collector()->mark_bitmap(), &compact);
 if (!hr->is_empty()) {
   MemRegion mr(hr->bottom(), hr->top());
   collector()->mark_bitmap()->clearRange(mr);
  }
  hr->complete_compaction();
}

size_t G1FullGCCompactTask::G1CompactRegionClosure::apply(oop obj) {
  size_t size = obj->size();
  HeapWord* destination = (HeapWord*)obj->forwardee();
  if (destination == NULL) {
    return size;
  }

  HeapWord* obj_addr = (HeapWord*) obj;
  assert(obj_addr != destination, "everything in this pass should be moving");
  Copy::aligned_conjoint_words(obj_addr, destination, size);
  oop(destination)->init_mark();
  assert(oop(destination)->klass() != NULL, "should have a class");

  return size;
}

inline void HeapRegion::complete_compaction() {
  ...
  if (used_region().is_empty()) {
    reset_bot();
  }
  ...
  if (ZapUnusedHeapArea) {
    SpaceMangler::mangle_region(MemRegion(top(), end()));
  }
}

G1 FullGC回收收尾——complete

在整個回收結束前,需要進行一些收尾工作,此處並無 parallel 區分。在 prepare_heap_for_mutators中,會包括重建 region 集合,將空的 region 加入 free_list;為每個 region 重建 strong root 列表;刪除已解除安裝類載入器的元空間,並清理 loader_data 圖;準備 collection set;重新處理卡表資訊以及一些驗證和資訊列印等。

 // openjdk11
void G1FullCollector::complete_collection() {
  。。。
  restore_marks();
  BiasedLocking::restore_marks();
  CodeCache::gc_epilogue();
  JvmtiExport::gc_epilogue();

  _heap->prepare_heap_for_mutators();
  ...
}

總結

相比於 YoungGC 和 Concurrent Marking Cycle 等,G1 回收器中 FullGC 過程更為獨立、完整,但涉及面也更廣,涉及到 JVM 對整個堆的管理細節,包括 OOP 物件結構,region 塊,bitmap 標記管理,起始 root 管理,甚至GC執行緒後臺執行情況,多執行緒同步,stw機制等等,每一部分的設計都很巧妙,值得我們深入程式碼中一探究竟,相信大家一定會有所收穫。