老年代TenuredGeneration所使用的垃圾回收算法是標記-壓縮-清理算法。在回收階段,將標記對象越過堆的空閑區移動到堆的另一端,所有被移動的對象的引用也會被更新指向新的位置。看起來像是把雜陳的箱子一股腦推到房間的一側一樣。 下面,從TenuredGeneration的collect()開始,分析TenuredGeneration的GC過程。
void TenuredGeneration::collect(bool full, bool clear_all_soft_refs, size_t size, bool is_tlab) { retire_alloc_buffers_before_full_gc(); OneContigSpaceCardGeneration::collect(full, clear_all_soft_refs, size, is_tlab); }
轉而調用了父類OneContigSpaceCardGeneration的collect():
void OneContigSpaceCardGeneration::collect(bool full, bool clear_all_soft_refs, size_t size, bool is_tlab) { SpecializationStats::clear(); // Temporarily expand the span of our ref processor, so // refs discovery is over the entire heap, not just this generation ReferenceProcessorSpanMutator x(ref_processor(), GenCollectedHeap::heap()->reserved_region()); GenMarkSweep::invoke_at_safepoint(_level, ref_processor(), clear_all_soft_refs); SpecializationStats::print(); }
接着看GenMarkSweep的invoke_at_safepoint():
1.前面的實現都是進行一些gc前的初始化工作和統計工作
(1).設置引用處理器和引用處理策略為clear_all_softrefs
_ref_processor = rp;
rp->setup_policy(clear_all_softrefs);
(2).增加調用計數,並統計gc前的堆的使用大小
gch->perm_gen()->stat_record()->invocations++; // Capture heap size before collection for printing. size_t gch_prev_used = gch->used();
(3).保存當前內存代和更低的內存代、以及永久代的已使用區域
gch->save_used_regions(level, true /* perm */);
(4).創建遍歷棧
allocate_stacks();
2.接下來就是MarkSweepCompact算法的實現了,算法的實現分為四個階段:
mark_sweep_phase1-4,其中:
mark_sweep_phase1:遞歸標記所有活躍對象
mark_sweep_phase2:計算所有活躍對象在壓縮后的偏移地址
mark_sweep_phase3:更新對象的引用地址
mark_sweep_phase4:移動所有活躍/存活對象到新的位置
mark_sweep_phase1(level, clear_all_softrefs); mark_sweep_phase2(); //... mark_sweep_phase3(level); //... mark_sweep_phase4();
3.在將對象標記入棧的時候,會將原MarkWord保存在_preserved_marks,MarkWord被設置為轉發指針,當四個處理階段結束后,恢復這些”廢棄”對象的MarkWord,以防止下次GC時干擾標記,雖然沒有真正“清空”死亡對象的內存空間,但由於對象引用將指向新的位置,原來的這些對象所占用內存空間將會被看作是空閑空間。
restore_marks();
保存各內存代的mark指針為當前空閑分配指針
gch->save_marks();
4.一些gc后的處理工作,例如清空所有遍歷棧、更新堆的一些使用信息和最近一次gc發生的時間等
mark_sweep_phase1:遞歸標記所有活躍對象
1.與新生代類似,標記根集對象。
follow_root_closure.set_orig_generation(gch->get_gen(level)); gch->gen_process_strong_roots(level, false, // Younger gens are not roots. true, // activate StrongRootsScope true, // Collecting permanent generation. SharedHeap::SO_SystemClasses, &follow_root_closure, true, // walk code active on stacks &follow_root_closure);
follow_root_closure的工作函數如下:
void MarkSweep::FollowRootClosure::do_oop(oop* p) { follow_root(p); } void MarkSweep::FollowRootClosure::do_oop(narrowOop* p) { follow_root(p); }
不考慮壓縮指針的解壓,follow_root()實現如下:
template <class T> inline void MarkSweep::follow_root(T* p) { assert(!Universe::heap()->is_in_reserved(p), "roots shouldn't be things within the heap"); //... 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 (!obj->mark()->is_marked()) { mark_object(obj); obj->follow_contents(); } } follow_stack(); }
對於沒有被標記的活躍對象,follow_root()會調用mark_object()標記該對象(設置轉發指針),隨后調用follow_contents()和follow_stack()處理該對象,根據借助棧進行遞歸標記的思想,遞歸標記的過程就是遍歷根集對象,把根集對象進行標記后,將其所引用的對象壓入棧,然后遍歷棧中元素,遞歸標記活躍對象及其所引用的對象,直至棧空為止。
oop_follow_contents()就應該是將當前活躍對象所引用的對象標記並壓入棧的過程:
void instanceKlass::oop_follow_contents(oop obj) { assert(obj != NULL, "can't follow the content of NULL object"); obj->follow_header(); InstanceKlass_OOP_MAP_ITERATE( \ obj, \ MarkSweep::mark_and_push(p), \ assert_is_in_closed_subset) }
InstanceKlass_OOP_MAP_ITERATE()語句塊在”源碼分析HotSpot GC過程(二):DefNewGeneration的GC過程“一文中已經分析過了,其作用就是遍歷對象的引用域,使用OopClosure進行處理。
故follow_contents()處理活躍對象就是將該對象標記后,將該對象所引用的對象標記后壓入_marking_stack。那么,可以預見,follow_stack()的處理必然就是遍歷棧中的對象,並遞歸地將其引用對象標記和入棧直到棧空為止,那么下面看看follow_stack()的具體實現:
void MarkSweep::follow_stack() { do { while (!_marking_stack.is_empty()) { oop obj = _marking_stack.pop(); assert (obj->is_gc_marked(), "p must be marked"); obj->follow_contents(); } // Process ObjArrays one at a time to avoid marking stack bloat. if (!_objarray_stack.is_empty()) { ObjArrayTask task = _objarray_stack.pop(); objArrayKlass* const k = (objArrayKlass*)task.obj()->blueprint(); k->oop_follow_contents(task.obj(), task.index()); } } while (!_marking_stack.is_empty() || !_objarray_stack.is_empty()); }
那么結果如何呢?好消息是,follow_stack()的前半段確實如此,壞消息是棧空了並不一定會結束,因為,光有一個_marking_stack棧是不夠的,對於數組對象的引用如果全都放在標記棧中時,當數組非常大時,就會出現爆棧的問題,這里就需要一個_objArrayKlass和一個ObjArrayTask用來處理數組對象的引用問題。具體的實現這里就不再深入下去了。
分析完活躍對象的處理過程,我們回到mark_sweep_phase1()中:
注意gen_process_strong_roots()傳入的younger_gens_as_roots參數為false,即不會對更低的內存代進行處理,因為在SharedHeap::process_strong_roots的處理過程中,就已經標記了所有的活躍對象。但是,如果存在更高內存代,那么更低內存代是無法將更高內存代的沒有被引用的對象當做垃圾對象處理的,所以雖然不會再處理更低的內存代,但仍要將更高內存代的對象當做根集對象遞歸遍歷。(Hotspot中TenuredGeneration沒有更高的內存代了)
2.遞歸標記發現的引用
// Process reference objects found during marking { ref_processor()->setup_policy(clear_all_softrefs); ref_processor()->process_discovered_references( &is_alive, &keep_alive, &follow_stack_closure, NULL); }
3.卸載不再使用的類
// Follow system dictionary roots and unload classes bool purged_class = SystemDictionary::do_unloading(&is_alive);
4.部分類卸載后,需要清理CodeCache,此外,需要清空標記棧
// Follow code cache roots CodeCache::do_unloading(&is_alive, &keep_alive, purged_class); follow_stack(); // Flush marking stack
5.部分類卸載后,更新存活類的子類、兄弟類、實現類的引用關系
follow_weak_klass_links();
6.清理未被標記的軟引用和弱引用
follow_mdo_weak_refs();
7.刪除拘留字符串表中未被標記的字符串對象
StringTable::unlink(&is_alive);
8.清理符號表中沒有被引用的符號
SymbolTable::unlink();
mark_sweep_phase2:計算所有活躍對象在壓縮后的偏移地址
void GenMarkSweep::mark_sweep_phase2() { GenCollectedHeap* gch = GenCollectedHeap::heap(); Generation* pg = gch->perm_gen(); //... VALIDATE_MARK_SWEEP_ONLY(reset_live_oop_tracking(false)); gch->prepare_for_compaction(); VALIDATE_MARK_SWEEP_ONLY(_live_oops_index_at_perm = _live_oops_index); CompactPoint perm_cp(pg, NULL, NULL); pg->prepare_for_compaction(&perm_cp); }
GenCollectedHeap的prepare_for_compaction()如下:
void GenCollectedHeap::prepare_for_compaction() { Generation* scanning_gen = _gens[_n_gens-1]; // Start by compacting into same gen. CompactPoint cp(scanning_gen, NULL, NULL); while (scanning_gen != NULL) { scanning_gen->prepare_for_compaction(&cp); scanning_gen = prev_gen(scanning_gen); } }
看到還記得在DefNewGeneration的GC分析中下一片壓縮區域的設置么? 根據內存代的不同實現,如DefNewGeneration分為Eden區(EdenSpace,ContiguousSpace的子類)、From/To區(ContiguousSpace),TenuredGeneration只有一個_the_space區(ContiguousSpace),這里直接看ContiguousSpace對prepare_for_compaction的實現:
// Faster object search. void ContiguousSpace::prepare_for_compaction(CompactPoint* cp) { SCAN_AND_FORWARD(cp, top, block_is_always_obj, obj_size); }
SCAN_AND_FORWARD(),該函數定義在/hotspot/src/share/vm/memory/space.hpp中
1.compact_top為壓縮指針,指向壓縮的目標內存空間的起始地址,在壓縮地址計算的開始,指向當前內存區域的起始位置
HeapWord* compact_top; /* This is where we are currently compacting to. */ \ \ /* We're sure to be here before any objects are compacted into this \ * space, so this is a good time to initialize this: \ */ \ set_compaction_top(bottom());
2.初始化CompactPoint,若CompactPoint的壓縮區域為空,即這是內存代的第一片區域,那么初始化CompactPoint的壓縮區域為內存代的第一片區域,初始化壓縮指針為區域的起始地址,初始化區域的壓縮的目標區域起始地址為該區域的起始地址,初始化壓縮邊界為區域邊界(默認實現),若CompactPoint的壓縮區域不為空,那么之前繼續進行該區域的壓縮工作,即初始化壓縮指針為原壓縮指針的值。
if (cp->space == NULL) { \ //assert cp->space = cp->gen->first_compaction_space(); \ compact_top = cp->space->bottom(); \ cp->space->set_compaction_top(compact_top); \ cp->threshold = cp->space->initialize_threshold(); \ } else { \ compact_top = cp->space->compaction_top(); \ }
3.每經過MarkSweepAlwaysCompactCount次GC,就允許當前區域空間的MarkSweepDeadRatio%(TenuredSpace)/PermMarkSweepDeadRatio%(ContigPermSpace)大小被用來將死亡對象當做存活對象處理,這里姑且將這些對象稱為彌留對象,把這片空間稱為彌留空間好了(實際上並沒有這樣的明確定義)。
nt invocations = SharedHeap::heap()->perm_gen()->stat_record()->invocations;\ bool skip_dead = ((invocations % MarkSweepAlwaysCompactCount) != 0); \ \ size_t allowed_deadspace = 0; \ if (skip_dead) { \ const size_t ratio = allowed_dead_ratio(); \ allowed_deadspace = (capacity() * ratio / 100) / HeapWordSize; \ }
4.q為遍歷指針,t為掃描邊界,end_of_live為最后一個活躍對象的地址,LiveRange保存着死亡對象后面存活對象的地址范圍,first_dead為第一個死亡對象的地址
HeapWord* q = bottom(); \ HeapWord* t = scan_limit(); \ \ HeapWord* end_of_live= q; /* One byte beyond the last byte of the last \ live object. */ \ HeapWord* first_dead = end();/* The first dead object. */ \ LiveRange* liveRange = NULL; /* The current live range, recorded in the \ first header of preceding free area. */ \ _first_dead = first_dead;
在邊界內遍歷,若當前遍歷的對象被標記過,即這是一個活躍對象,那么為該對象計算壓縮后的地址,設置轉發指針,並更新壓縮指針和最后一個活躍對象的地址,並繼續遍歷
while (q < t) { if (block_is_obj(q) && oop(q)->is_gc_marked()) { \ /* prefetch beyond q */ \ Prefetch::write(q, interval); \ /* size_t size = oop(q)->size(); changing this for cms for perm gen */\ size_t size = block_size(q); \ compact_top = cp->space->forward(oop(q), size, cp, compact_top); \ q += size; \ end_of_live = q; \ }
否則,跳過死亡對象,遍歷直到遇到一個活躍對象為止
else { \ /* run over all the contiguous dead objects */ \ HeapWord* end = q; \ do { \ /* prefetch beyond end */ \ Prefetch::write(end, interval); \ end += block_size(end); \ } while (end < t && (!block_is_obj(end) || !oop(end)->is_gc_marked()));
若仍有彌留空間可以用,那么在這片空間上調用insert_deadspace()構造彌留對象,當做活躍對象進行壓縮的計算處理
if (allowed_deadspace > 0 && q == compact_top) { \ size_t sz = pointer_delta(end, q); \ if (insert_deadspace(allowed_deadspace, q, sz)) { \ compact_top = cp->space->forward(oop(q), sz, cp, compact_top); \ q = end; \ end_of_live = end; \ continue; \ } \ }
更新上一個LiveRange的活躍對象結束地址,這個活躍范圍對象設置在死亡對象的MarkWord上,由於在死亡對象后遇到了一個新的活躍對象,於是需要重新構造一個LiveRange對象來記錄下一片活躍對象的地址范圍。
if (liveRange) { \ liveRange->set_end(q); \ } liveRange = (LiveRange*)q; \ liveRange->set_start(end); \ liveRange->set_end(end);
保存首個死亡對象的地址,並繼續遍歷
/* see if this is the first dead region. */ \ if (q < first_dead) { \ first_dead = q; \ } \ \ /* move on to the next object */ \ q = end; \ }
循環結束,更新最后一個死亡對象的活躍對象范圍、最后一個活躍對象的地址、第一個死亡對象的地址
if (liveRange != NULL) { \ liveRange->set_end(q); \ } \ _end_of_live = end_of_live; \ if (end_of_live < first_dead) { \ first_dead = end_of_live; \ } \ _first_dead = first_dead;
保存當前空間的壓縮指針
cp->space->set_compaction_top(compact_top);
mark_sweep_phase3:更新對象的引用地址
1.adjust_root_pointer_closure和adjust_pointer_closure都是靜態創建的對象引用地址調整函數的封裝對象,這里將調用gen_process_strong_roots()並使用這兩個處理函數調整根集對象指針的引用地址。
adjust_root_pointer_closure.set_orig_generation(gch->get_gen(level)); adjust_pointer_closure.set_orig_generation(gch->get_gen(level)); gch->gen_process_strong_roots(level, false, // Younger gens are not roots. true, // activate StrongRootsScope true, // Collecting permanent generation. SharedHeap::SO_AllClasses, &adjust_root_pointer_closure, false, // do not walk code &adjust_root_pointer_closure);
adjust_root_pointer_closure()的工作函數如下:
void MarkSweep::AdjustPointerClosure::do_oop(oop* p) { adjust_pointer(p, _is_root); } void MarkSweep::AdjustPointerClosure::do_oop(narrowOop* p) { adjust_pointer(p, _is_root); }
MarkSweep的adjust_pointer將會解析引用對象的MarkWord,若該引用對象已經被標記,就會解析轉發指針,並設置引用地址為引用對象新的地址。
template <class T> inline void MarkSweep::adjust_pointer(T* p, bool isroot) { T heap_oop = oopDesc::load_heap_oop(p); if (!oopDesc::is_null(heap_oop)) { oop obj = oopDesc::decode_heap_oop_not_null(heap_oop); oop new_obj = oop(obj->mark()->decode_pointer()); //...assert if (new_obj != NULL) { //...assert oopDesc::encode_store_heap_oop_not_null(p, new_obj); } } VALIDATE_MARK_SWEEP_ONLY(track_adjusted_pointer(p, isroot)); }
所以對引用地址的更新就是遍歷各內存代對象/引用,若對象所引用的對象已經被標記,則更新其引用地址為轉發指針所轉向的新地址。
gen_process_strong_roots()完成了對初始根對象的引用地址更新
2.調整引用指針的引用地址
CodeBlobToOopClosure adjust_code_pointer_closure(&adjust_pointer_closure, /*do_marking=*/ false); gch->gen_process_weak_roots(&adjust_root_pointer_closure, &adjust_code_pointer_closure, &adjust_pointer_closure);
3.使用GenAdjustPointersClosure遍歷各內存代,以更新引用對象的引用地址
GenAdjustPointersClosure blk; gch->generation_iterate(&blk, true); pg->adjust_pointers();
其基本思想如圖所示:
mark_sweep_phase4:移動所有active對象到新的位置
1.永久代對象壓縮,只有在永久代對象壓縮后,實例才能獲得正確的類數據地址
pg->compact();
2.使用GenCompactClosure遍歷堆上的對象
GenCompactClosure blk; gch->generation_iterate(&blk, true);
GenCollectedHeap的generation_iterate()將調用GenCompactClosure的do_generation()遍歷各個內存代
void GenCollectedHeap::generation_iterate(GenClosure* cl, bool old_to_young) { if (old_to_young) { for (int i = _n_gens-1; i >= 0; i--) { cl->do_generation(_gens[i]); } } else { for (int i = 0; i < _n_gens; i++) { cl->do_generation(_gens[i]); }
do_generation()實際上是調用各個內存代的compact()進行處理(因為各個內存代的區域組織形式不同,比如新生代有Eden和From/To區,而老年代只有一個區域存在)
class GenCompactClosure: public GenCollectedHeap::GenClosure { public: void do_generation(Generation* gen) { gen->compact(); } };
compact()調用了CompactibleSpace(ContiguousSpace的父類)的SCAN_AND_COMPACT()完成對象內容的復制
void CompactibleSpace::compact() { SCAN_AND_COMPACT(obj_size); }
SCAN_AND_COMPACT()定義在/hotspot/src/share/vm/memory/space.hpp中
(1).q是遍歷指針,t是最后一個活躍對象的位置,記錄最后一個活躍對象的位置,就不必再遍歷全部內存區域,否則當gc后剩余的活躍對象較少時,將會進行很多不必要的遍歷
HeapWord* q = bottom(); \ HeapWord* const t = _end_of_live; \
(2).跳過死亡對象區域
if (q < t && _first_dead > q && \ !oop(q)->is_gc_marked()) { \ //... HeapWord* const end = _first_dead; \ \ while (q < end) { \ size_t size = obj_size(q); \ //...assert q += size; \ }
當第一個死亡對象的地址與最后一個活躍對象的地址不相同時,即有連續多個死亡對象存在,那么第一個死亡對象的MarkWord就是之前保存的LiveRange,通過LiveRange可以獲取下一個活躍對象的地址
if (_first_dead == t) { \ q = t; \ } else { \ /* $$$ Funky */ \ q = (HeapWord*) oop(_first_dead)->mark()->decode_pointer(); \ }
(3).開始遍歷,對於死亡對象,同樣通過LiveRange獲取下一個存活對象的地址
const intx scan_interval = PrefetchScanIntervalInBytes; \ const intx copy_interval = PrefetchCopyIntervalInBytes; \ while (q < t) { \ if (!oop(q)->is_gc_marked()) { \ /* mark is pointer to next marked oop */ \ q = (HeapWord*) oop(q)->mark()->decode_pointer(); \ }
(4).復制原對象的數據內容到壓縮后的地址,並初始化新的位置的對象的MarkWord
else { \ /* prefetch beyond q */ \ Prefetch::read(q, scan_interval); \ \ /* size and destination */ \ size_t size = obj_size(q); \ HeapWord* compaction_top = (HeapWord*)oop(q)->forwardee(); \ \ /* prefetch beyond compaction_top */ \ Prefetch::write(compaction_top, copy_interval); \ \ //... Copy::aligned_conjoint_words(q, compaction_top, size); \ oop(compaction_top)->init_mark(); \ //... q += size; \ }
我們以如下分代和引用模型為基礎進行TenuredGeneration的GC分析:
其中藍色對象為正常的根對象,箭頭代表引用關系。
1.MarkSweepPhase1過程分為兩步,第一步是遞歸標記所有根對象:
以根集對象C1為例,借助標記棧的標記過程如下,其中橙色對象為被標記的正常的根對象,綠色為被標記的其他對象:
第二步是遞歸標記作為更高內存代的對象,這里即為H3,和其所引用的C7對象:
2.MarkSweepPhase2過程,以TenuredGeneration為代表進行分析:
C3、C5、C9對象均為垃圾對象,由於前面沒有垃圾對象,C1、C2的轉發指針均指向自身所在地址
而C3是垃圾對象,C3的MarkWord被設置為LiveRange,指向前一塊活躍對象的范圍,而C4的轉發指針將壓縮到C3的起始地址
以此類推,計算所有活躍對象的轉發地址和活躍范圍,注意這里壓縮指針位置是根據前一次的指針位置和前一個活躍對象的大小計算的
3.MarkSweepPhase3過程,這里C4、C6、C7、C8的壓縮后地址均已發生變化,但對象內容尚未復制到新地址,所以以虛線邊框淺色來代表這個”名義上存在的”新對象,並更新引用這些對象的指針地址:
4.MarkSweepPhase3過程,C4、C6、C7、C8的對象內容復制到新地址。