深入分析 OpenJDK G1 FullGC 原理
導讀
本文主要從程式碼層面介紹 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 業務中使用較為頻繁的垃圾回收器。但隨著使用場景的變換,為了在業務中更好地完成垃圾回收調優,可以更多地瞭解下這項技術的基礎知識。
-
G1 將整個 JVM 執行時堆分為若干個 region,region 大小在執行之初設定,且一經設定不可改變;每個 region 都可能被作以下分別:包括 Eden,Survivor,Old,Humongous 和 Free 等區域,其中 Eden region 指年輕代,一般物件(大小小於 region 的一半)物件分配的預設選擇區域,並且為每次記憶體空間不足時的優先回收區;從一次垃圾回收存活下來的物件會被 copy 到 survivor region,經過幾輪迴收後,依然存活的物件太多,則這些物件會被移動到 Old region;當物件大小大於region 一半會被直接分配到 Humongous region,只有在 Full GC 或者 CMS 後才有機會回收。
-
回收包含 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。
-
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機制等等,每一部分的設計都很巧妙,值得我們深入程式碼中一探究竟,相信大家一定會有所收穫。