part 2
這個comment希望能分析一下GenCollectedHeap::do_collection這個函數的具體執行流程,根據函數名字可以猜測該函數實現的功能就是做垃圾回收,下面是它的方法聲明(聲明和定義是有區別的,聲明僅僅是告訴別人有這樣一個函數,而定義則是說這個函數具體實現了什么功能):
// Helper function for two callbacks below.
// Considers collection of the first max_level+1 generations.
void do_collection(bool full,
bool clear_all_soft_refs,
size_t size,
bool is_tlab,
GenerationType max_generation);
參數full代表是否是FullGC,clear_all_soft_refs參數表示是否要回收sort reference,size參數需要多說明一點,在一些情況下,GC發生是因為發送了"Allocate Fail",這個size就代表了申請分配的內存大小;is_tlab表示是否使用 TLAB(線程分配Buffer,可以避免多線程在堆上并發申請內存),max_generation參數表示最大的回收代,只有兩種類型,YoungGen或者OldGen;下面來仔細分析一下這個函數。
- (1)、首先是做一些基本的校驗,比如是否在safe_point,是否是GC線程訪問該函數,以及是否已經有其他的線程觸發了GC,這些條件都需要滿足才能執行接下來的代碼。
- (2)、接下來需要做一些GC策略的生成,主要是判斷是否回收soft reference對象,是否收集Young或者Old區域等。complete表示是否收集整個堆,old_collects_young表示是否在收集老年代的同時收集新生代,也就是是否有必要收集新生代,JVM參數ScavengeBeforeFullGC控制是否在FullGC前做一次YoungGC,如果設置了該參數,那在收集old區的時候就沒有必要再回收young區了;do_young_collection表示是否需要對young區域進行垃圾收集,判斷標準就是young區域確實需要回收了,也就是進行YoungGC,
- (3)、現在,知道該回收哪些區域了,那么接下來就去回收需要回收的區域,如果do_young_collection是true的,那么就執行YoungGC,collect_generation函數是具體的執行某個區域垃圾回收的入口,待會再來分析這個函數的具體流程;接著也判讀oldGen是否需要回收,如果需要的話也進行回收。
- (4)、垃圾收集完成之后,需要計算各個分代的大小因為GC之后堆可能會擴展,所以需要重新計算一下各個分代的大小,重新計算大小通過調用函數compute_new_size實現,該函數需要調整各個分代的各種指針,使得堆擴展后各個分代依然可以正常工作。
下面,來分析上面幾個步驟中出現的一些關鍵函數,首先是should_collect函數,該函數用于判斷某一個分代是否需要做垃圾回收下面來看看該方法的細節
// Returns "true" iff collect() should subsequently be called on this
// this generation. See comment below.
// This is a generic implementation which can be overridden.
//
// Note: in the current (1.4) implementation, when genCollectedHeap's
// incremental_collection_will_fail flag is set, all allocations are
// slow path (the only fast-path place to allocate is DefNew, which
// will be full if the flag is set).
// Thus, older generations which collect younger generations should
// test this flag and collect if it is set.
virtual bool should_collect(bool full,
size_t word_size,
bool is_tlab) {
return (full || should_allocate(word_size, is_tlab));
}
如果是FullGC,那么無論哪個分代都應該被回收,如果不是FullGC,那么就使用should_allocate函數繼續判斷是否需要在該分代進行收集,比如對于DefNew(Serial GC下新生代)分代來說,其具體實現就如下:
// Allocation support
virtual bool should_allocate(size_t word_size, bool is_tlab) {
assert(UseTLAB || !is_tlab, "Should not allocate tlab");
size_t overflow_limit = (size_t)1 << (BitsPerSize_t - LogHeapWordSize);
const bool non_zero = word_size > 0;
const bool overflows = word_size >= overflow_limit;
const bool check_too_big = _pretenure_size_threshold_words > 0;
const bool not_too_big = word_size < _pretenure_size_threshold_words;
const bool size_ok = is_tlab || !check_too_big || not_too_big;
bool result = !overflows &&
non_zero &&
size_ok;
return result;
}
接著一個重要的函數就是collect_generation,這個函數將回收給定的分代中的垃圾,主要看下面的這段代碼片段:
// Do collection work
{
// Note on ref discovery: For what appear to be historical reasons,
// GCH enables and disabled (by enqueing) refs discovery.
// In the future this should be moved into the generation's
// collect method so that ref discovery and enqueueing concerns
// are local to a generation. The collect method could return
// an appropriate indication in the case that notification on
// the ref lock was needed. This will make the treatment of
// weak refs more uniform (and indeed remove such concerns
// from GCH). XXX
HandleMark hm; // Discard invalid handles created during gc
save_marks(); // save marks for all gens
// We want to discover references, but not process them yet.
// This mode is disabled in process_discovered_references if the
// generation does some collection work, or in
// enqueue_discovered_references if the generation returns
// without doing any work.
ReferenceProcessor* rp = gen->ref_processor();
// If the discovery of ("weak") refs in this generation is
// atomic wrt other collectors in this configuration, we
// are guaranteed to have empty discovered ref lists.
if (rp->discovery_is_atomic()) {
rp->enable_discovery();
rp->setup_policy(clear_soft_refs);
} else {
// collect() below will enable discovery as appropriate
}
gen->collect(full, clear_soft_refs, size, is_tlab);
if (!rp->enqueuing_is_done()) {
rp->enqueue_discovered_references();
} else {
rp->set_enqueuing_is_done(false);
}
rp->verify_no_references_recorded();
}
接著看gen->collect函數調用,這里面就是做具體的垃圾收集工作,比如下面分析在DefNew分代中的gen->collect實現。
- (1)、DefNew是Serial GC下的新生代,首先它要判斷是否有必要讓老年代來做這次GC,使用collection_attempt_is_safe函數來做這個判斷,也就是判斷出收集該區域是否是安全的,所謂安全的,就是DefNew分代收集了之后,old 區域是否可以完整的將這次Minor GC之后晉升的對象安置起來,如果不能的話,那DefNew就舉得自己做GC是不安全的,應該讓老年代來做GC,這也是最合適的選擇,老年代會做一次規模宏大的GC,并且做一些內存規整的工作,避免新生代中晉升上來的大對象無法找到連續的空間放置,當然,老年代GC實現上幾乎都包含"整理"的階段,這也是為什么老年代發生GC耗時是新生代GC的10倍的原因之一,新生代使用copying算法,是一種非常快速的收集算法,當然也得益于新生代中的對象壽命都比較短,不像老年代中的對象壽命較長,當然,這也是分代的意義所在;
[圖片上傳失敗...(image-f91c09-1627873692281)]
collection_attempt_is_safe函數的實現如下:
bool DefNewGeneration::collection_attempt_is_safe() {
if (!to()->is_empty()) {
log_trace(gc)(":: to is not empty ::");
return false;
}
if (_old_gen == NULL) {
GenCollectedHeap* gch = GenCollectedHeap::heap();
_old_gen = gch->old_gen();
}
return _old_gen->promotion_attempt_is_safe(used());
}
正常來說,用區域內兩個survivor中有一個區域總是空閑的,但是在某些情況下也會發生意外,使得兩個survivor都不為空,這種情況是有可能發生的,首先DefNew在進行YoungGC之后,會將Eden + From中存活的對象拷貝到To中去,并且將一些符合晉升要求的對象拷貝到old區域中去,然后調換兩個survivor的角色,所以按理來說其中某個survivor區域總是空的,但是這是在YoungGC順利完成的情況,在發生"promotion failed"的時候就不會去清理From和To,這一點在后續會再次說明;但是肯定的是,如果To區域不為空,那么就說明前一次YoungGC并不是很順利,此時DefNew就舉得沒必要再冒險去做一次可能沒啥用處的Minor GC,因為有可能Minor GC之后需要出發一次Full GC來解決某些難題,所以DefNew基于自己的歷史GC告訴Old去做一些較為徹底的GC工作時必要的;如果沒有發生"promotion fail"這種不愉快的事情,那么接下來就讓old區自己判斷是否允許本次Minor GC的發生,也就是_old_gen->promotion_attempt_is_safe的調用,下面來看看該函數的具體實現;
bool TenuredGeneration::promotion_attempt_is_safe(size_t max_promotion_in_bytes) const {
size_t available = max_contiguous_available();
size_t av_promo = (size_t)gc_stats()->avg_promoted()->padded_average();
bool res = (available >= av_promo) || (available >= max_promotion_in_bytes);
log_trace(gc)("Tenured: promo attempt is%s safe: available(" SIZE_FORMAT ") %s av_promo(" SIZE_FORMAT "), max_promo(" SIZE_FORMAT ")",
res? "":" not", available, res? ">=":"<", av_promo, max_promotion_in_bytes);
return res;
}
老年代也會看歷史數據,如果發現老年代的最大連續空間大小大于新生代歷史晉升的平均大小或者新生代中存活的對象,那么老年代就認為本次Minor GC是安全的,沒必要做一次Full GC;當然這是有一些冒險的成分的,如果某一次minorGC發生之后符合晉升條件的對象大小遠遠大小評價晉升大小,而且這個時候老年代連續空間小于這些符合晉升的對象大小的時候,悲劇就發生了,也就是上面說到的"promotion fail",這個時候就要做一次Full GC。
- (2)、接著關鍵的一個步驟就是進行對象存活判斷,并且將存活的對象轉移到正確的位置,比如To區域或者old區域;
FastEvacuateFollowersClosure是一個遞歸的過程,Closure后綴代表 它是一個回調操作,所謂遞歸,就是在判斷對象存活并且copying的工作是遞歸進行的,首先找到root objects,然后根據root objects去標記存活的對象,并且將它們轉移到合適的區域中去;gch->young_process_roots做的工作就是將root objects轉移到其他空間去的函數:
void GenCollectedHeap::young_process_roots(StrongRootsScope* scope,
OopsInGenClosure* root_closure,
OopsInGenClosure* old_gen_closure,
CLDClosure* cld_closure) {
MarkingCodeBlobClosure mark_code_closure(root_closure, CodeBlobToOopClosure::FixRelocations);
process_roots(scope, SO_ScavengeCodeCache, root_closure, root_closure,
cld_closure, cld_closure, &mark_code_closure);
process_string_table_roots(scope, root_closure);
if (!_process_strong_tasks->is_task_claimed(GCH_PS_younger_gens)) {
root_closure->reset_generation();
}
// When collection is parallel, all threads get to cooperate to do
// old generation scanning.
old_gen_closure->set_generation(_old_gen);
rem_set()->younger_refs_iterate(_old_gen, old_gen_closure, scope->n_threads());
old_gen_closure->reset_generation();
_process_strong_tasks->all_tasks_completed(scope->n_threads());
}
這里面關鍵的函數是process_roots,該函數會對設置的各種Closure進行回調,比如FastScanClosure,具體的回調工作將在Closure的do_oop_work進行:
// NOTE! Any changes made here should also be made
// in ScanClosure::do_oop_work()
template <class T> inline void FastScanClosure::do_oop_work(T* p) {
T heap_oop = oopDesc::load_heap_oop(p);
// Should we copy the obj?
if (!oopDesc::is_null(heap_oop)) {
oop obj = oopDesc::decode_heap_oop_not_null(heap_oop);
if ((HeapWord*)obj < _boundary) {
assert(!_g->to()->is_in_reserved(obj), "Scanning field twice?");
oop new_obj = obj->is_forwarded() ? obj->forwardee()
: _g->copy_to_survivor_space(obj);
oopDesc::encode_store_heap_oop_not_null(p, new_obj);
if (is_scanning_a_klass()) {
do_klass_barrier();
} else if (_gc_barrier) {
// Now call parent closure
do_barrier(p);
}
}
}
}
如果對象已經被復制過了,那么就不用再復制一次了,否則調用copy_to_survivor_space將該對象復制到to區域中去,下面是copy_to_survivor_space函數的具體實現:
oop DefNewGeneration::copy_to_survivor_space(oop old) {
assert(is_in_reserved(old) && !old->is_forwarded(),
"shouldn't be scavenging this oop");
size_t s = old->size();
oop obj = NULL;
// Try allocating obj in to-space (unless too old)
if (old->age() < tenuring_threshold()) {
obj = (oop) to()->allocate_aligned(s);
}
// Otherwise try allocating obj tenured
if (obj == NULL) {
obj = _old_gen->promote(old, s);
if (obj == NULL) {
handle_promotion_failure(old);
return old;
}
} else {
// Prefetch beyond obj
const intx interval = PrefetchCopyIntervalInBytes;
Prefetch::write(obj, interval);
// Copy obj
Copy::aligned_disjoint_words((HeapWord*)old, (HeapWord*)obj, s);
// Increment age if obj still in new generation
obj->incr_age();
age_table()->add(obj, s);
}
// Done, insert forward pointer to obj in this header
old->forward_to(obj);
return obj;
}
這個函數的流程大概是這樣的:首先判斷對象是否達到了晉升到老年代的年齡閾值,如果到了,那么就要將對象拷貝到老年代中去,否則就要將對象拷貝到to區域中去,這里面也包括一個細節,如果對象沒有達到晉升老年代的年齡閾值,但是無法拷貝到To區域中去,那么也試圖將對象晉升到老年代,也就是將對象提前晉升,晉升是有風險的,可能晉升失敗,那么就要通過調用handle_promotion_failure來處理晉升失敗的情況,如果對象成功拷貝到了To區域中來,那么就要將對象的年齡更新一下,最后,需要需要標記對象已經被轉移,如果可能,那么就把老的對象清空吧;下面來先來看看promote函數,該函數用于將對象晉升到老年代:
// Ignores "ref" and calls allocate().
oop Generation::promote(oop obj, size_t obj_size) {
assert(obj_size == (size_t)obj->size(), "bad obj_size passed in");
#ifndef PRODUCT
if (GenCollectedHeap::heap()->promotion_should_fail()) {
return NULL;
}
#endif // #ifndef PRODUCT
HeapWord* result = allocate(obj_size, false);
if (result != NULL) {
Copy::aligned_disjoint_words((HeapWord*)obj, result, obj_size);
return oop(result);
} else {
GenCollectedHeap* gch = GenCollectedHeap::heap();
return gch->handle_failed_promotion(this, obj, obj_size);
}
}
這個函數較為簡單,首先通過allocate函數試圖在老年代申請一塊可以容納對象的內存,如果成功了,那么就將對象復制到里面去,否則通過handle_failed_promotion函數來處理晉升失敗的情況,晉升失敗的前提下,handle_failed_promotion在handle_promotion_failure前執行,看起來都是處理晉升失敗的情況,下面先看看handle_failed_promotion:
oop GenCollectedHeap::handle_failed_promotion(Generation* old_gen,
oop obj,
size_t obj_size) {
guarantee(old_gen == _old_gen, "We only get here with an old generation");
assert(obj_size == (size_t)obj->size(), "bad obj_size passed in");
HeapWord* result = NULL;
result = old_gen->expand_and_allocate(obj_size, false);
if (result != NULL) {
Copy::aligned_disjoint_words((HeapWord*)obj, result, obj_size);
}
return oop(result);
}
可以看到,oldGen將試圖去擴展自己的堆空間來讓更多的新生代對象可以成功晉升,但是很多情況下,堆空間被設置為不可擴展,這種情況下這個方法也就做了無用功,接著會調用handle_promotion_failure,調用handle_promotion_failure代表老年代也就明確告訴新生代無法將本次晉升的這個對象放置到老年代,來看看handle_promotion_failure會有什么對策:
void DefNewGeneration::handle_promotion_failure(oop old) {
log_debug(gc, promotion)("Promotion failure size = %d) ", old->size());
_promotion_failed = true;
_promotion_failed_info.register_copy_failure(old->size());
_preserved_marks_set.get()->push_if_necessary(old, old->mark());
// forward to self
old->forward_to(old);
_promo_failure_scan_stack.push(old);
if (!_promo_failure_drain_in_progress) {
// prevent recursion in copy_to_survivor_space()
_promo_failure_drain_in_progress = true;
drain_promo_failure_scan_stack();
_promo_failure_drain_in_progress = false;
}
}
看起來DefNew還是比較樂觀的,既然老年代容納不了你,那么這個晉升的對象就還呆在新生代吧,說不定下次老年代發生GC就可以成功把它拷貝過去呢。這個時候_promotion_failed也被標記物為了true,這個標記之后會有用,發生"promotion fail"之后From區域可能存在一些對象沒有成功晉升到老年代,但是又不是垃圾,這個時候From和To區域都不為空了,這是個難題。
接著,是時候執行遞歸標記&復制的過程了,也就是evacuate_followers.do_void(),這個過程是非常復雜的,下面來稍微看看這個函數:
void DefNewGeneration::FastEvacuateFollowersClosure::do_void() {
do {
_gch->oop_since_save_marks_iterate(GenCollectedHeap::YoungGen, _scan_cur_or_nonheap, _scan_older);
} while (!_gch->no_allocs_since_save_marks());
guarantee(_young_gen->promo_failure_scan_is_complete(), "Failed to finish scan");
}
不斷使用oop_since_save_marks_iterate來做遞歸遍歷的工作,結束條件是通過no_allocs_since_save_marks來決定的,下面是no_allocs_since_save_marks函數的具體實現:
bool GenCollectedHeap::no_allocs_since_save_marks() {
return _young_gen->no_allocs_since_save_marks() &&
_old_gen->no_allocs_since_save_marks();
}
看名字應該是說沒有分配發生了,比如看看DefNew的no_allocs_since_save_marks函數實現:
bool DefNewGeneration::no_allocs_since_save_marks() {
assert(eden()->saved_mark_at_top(), "Violated spec - alloc in eden");
assert(from()->saved_mark_at_top(), "Violated spec - alloc in from");
return to()->saved_mark_at_top();
}
top()指向To區域空閑空間的起點,上面已經說過的一個過程是將root objects先標記并且拷貝到To區域或者老年代,這個時候To區域內已經存在的對象是存活的,需要遞歸遍歷這些對象引用的對象,然后也進行拷貝工作,saved_mark_at_top就是判斷是否還在有對象唄拷貝到To區域中來,如果還有對象拷貝進來,那么就說明GC還沒有完成,繼續循環執行oop_since_save_marks_iterate,否則就可以停止了;下面來看看oop_since_save_marks_iterate函數的實現:
#define ContigSpace_OOP_SINCE_SAVE_MARKS_DEFN(OopClosureType, nv_suffix) \
\
void ContiguousSpace:: \
oop_since_save_marks_iterate##nv_suffix(OopClosureType* blk) { \
HeapWord* t; \
HeapWord* p = saved_mark_word(); \
assert(p != NULL, "expected saved mark"); \
\
const intx interval = PrefetchScanIntervalInBytes; \
do { \
t = top(); \
while (p < t) { \
Prefetch::write(p, interval); \
debug_only(HeapWord* prev = p); \
oop m = oop(p); \
p += m->oop_iterate_size(blk); \
} \
} while (t < top()); \
\
set_saved_mark_word(p); \
}
ALL_SINCE_SAVE_MARKS_CLOSURES(ContigSpace_OOP_SINCE_SAVE_MARKS_DEFN)
在深入下去的部分就比較復雜了,不再做分析,但是需要注意的一點是,DefNew在將存活對象復制到To區域的時候,Eden + From區域的對象是否存活不僅僅會看是否被To區域的對象引用,還會看老年代是否存在跨代引用新生代的對象的情況,這種情況也需要將存活的對象轉到To或者老年代。
- (3)、接下來需要對GC過程中發現的引用進行一些處理,比如是否回收soft reference,以及堆weak reference的回收等工作;
- (4)、到此GC工作大概已經完成了,接下來需要做一些收尾工作,如果發現在Minor GC的過程中發生了"promotion fail",那么就要做特殊的處理,younger_refs_iterate會將那些晉升失敗的對象恢復回來,否則下一次發生Minor GC的時候會誤以為這些對象已經被復制過了,但是他們確實沒有被轉移成功,這樣的話,這些對象可能一直留在新生代,無論經歷多少次GC都無法發生轉移;
[圖片上傳失敗...(image-b054f0-1627873692281)]
無論如何,新生代發生了GC,經過這次GC,需要轉換From和To兩個survivor的角色,swap_spaces函數實現了這個功能:
void DefNewGeneration::swap_spaces() {
ContiguousSpace* s = from();
_from_space = to();
_to_space = s;
eden()->set_next_compaction_space(from());
// The to-space is normally empty before a compaction so need
// not be considered. The exception is during promotion
// failure handling when to-space can contain live objects.
from()->set_next_compaction_space(NULL);
if (UsePerfData) {
CSpaceCounters* c = _from_counters;
_from_counters = _to_counters;
_to_counters = c;
}
}
這個函數較為簡單,只是swap了一下From和To;再說一句,如果沒有發生"Promotion Fail",那么在Minor GC之后,需要將From和Eden清空,因為沒有發生晉升失敗事件,就說明所以在新生代(Eden + From)存活的對象都安全的轉移到了To或者老年代,所以可以清空,但是發生晉升失敗意味著有部分存活的對象依然還留在原地等待,所以不能clear掉。