G1的RSET
本文的一些图片和源码 来自于 https://www.jianshu.com/p/870abddaba41
https://blog.csdn.net/baiye_xing/article/details/73743395
前一篇介绍了SATB,SATB属于pre write barrier。而今天要介绍的remembered set是一种post write barrier.
RSet
每个Region初始化时,会初始化一个remembered set(已记忆集合),这个翻译有点拗口,以下简称RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512b划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。

RSet实现过程
为了维护这些RSet,如果每次给引用类型的字段赋值都要更新RSet,这带来的额外开销实在太大,G1中采用post-write barrier和concurrent refinement threads实现了RSet的更新。
//假设对象young和old分别在不同的Region中 Object young = new Object(); old.p = young;
只有old引用young才需要记录rset,所以例子才会这么举。
在赋值动作的前后,JVM插入一个pre-write barrier和post-write barrier,其中post-write barrier的最终动作如下:

- 找到该字段所在的位置(Card),并设置为dirty_card
- 如果当前是应用线程,每个Java线程有一个dirty card queue,把该card插入队列
- 除了每个线程自带的dirty card queue,还有一个全局共享的queue
G1RemSet::refine_card方法,大概实现逻辑如下:1、根据card的地址,计算出card所在的Region
2、如果Region不存在,或者Region是Young区,或者该Region在回收集合中,则不进行处理
3、最终使用闭合函数
G1UpdateRSOrPushRefOopClosure::do_oop_nv()的处理该card中的对象inline void G1UpdateRSOrPushRefOopClosure::do_oop_nv(T* p) { oop obj = oopDesc::load_decode_heap_oop(p); if (obj == NULL) { return; } #ifdef ASSERT // can't do because of races // assert(obj == NULL || obj->is_oop(), "expected an oop"); // Do the safe subset of is_oop #ifdef CHECK_UNHANDLED_OOPS oopDesc* o = obj.obj(); #else oopDesc* o = obj; #endif // CHECK_UNHANDLED_OOPS assert((intptr_t)o % MinObjAlignmentInBytes == 0, "not oop aligned"); assert(Universe::heap()->is_in_reserved(obj), "must be in heap"); #endif // ASSERT assert(_from != NULL, "from region must be non-NULL"); assert(_from->is_in_reserved(p), "p is not in from"); HeapRegion* to = _g1->heap_region_containing(obj); if (_from == to) { // Normally this closure should only be called with cross-region references. // But since Java threads are manipulating the references concurrently and we // reload the values things may have changed. return; } // The _record_refs_into_cset flag is true during the RSet // updating part of an evacuation pause. It is false at all // other times: // * rebuilding the remembered sets after a full GC // * during concurrent refinement. // * updating the remembered sets of regions in the collection // set in the event of an evacuation failure (when deferred // updates are enabled). if (_record_refs_into_cset && to->in_collection_set()) { // We are recording references that point into the collection // set and this particular reference does exactly that... // If the referenced object has already been forwarded // to itself, we are handling an evacuation failure and // we have already visited/tried to copy this object // there is no need to retry. if (!self_forwarded(obj)) { assert(_push_ref_cl != NULL, "should not be null"); // Push the reference in the refs queue of the G1ParScanThreadState // instance for this worker thread. _push_ref_cl->do_oop(p); } // Deferred updates to the CSet are either discarded (in the normal case), // or processed (if an evacuation failure occurs) at the end // of the collection. // See G1RemSet::cleanup_after_oops_into_collection_set_do(). } else { // We either don't care about pushing references that point into the // collection set (i.e. we're not during an evacuation pause) _or_ // the reference doesn't point into the collection set. Either way // we add the reference directly to the RSet of the region containing // the referenced object. assert(to->rem_set() != NULL, "Need per-region 'into' remsets."); to->rem_set()->add_reference(p, _worker_i); } }
OtherRegionsTable::add_reference方法中void OtherRegionsTable::add_reference(OopOrNarrowOopStar from, int tid) { uint cur_hrm_ind = hr()->hrm_index(); if (G1TraceHeapRegionRememberedSet) { gclog_or_tty->print_cr("ORT::add_reference_work(" PTR_FORMAT "->" PTR_FORMAT ").", from, UseCompressedOops ? (void *)oopDesc::load_decode_heap_oop((narrowOop*)from) : (void *)oopDesc::load_decode_heap_oop((oop*)from)); } int from_card = (int)(uintptr_t(from) >> CardTableModRefBS::card_shift); if (G1TraceHeapRegionRememberedSet) { gclog_or_tty->print_cr("Table for [" PTR_FORMAT "...): card %d (cache = "INT32_FORMAT")", hr()->bottom(), from_card, FromCardCache::at((uint)tid, cur_hrm_ind)); } if (FromCardCache::contains_or_replace((uint)tid, cur_hrm_ind, from_card)) { if (G1TraceHeapRegionRememberedSet) { gclog_or_tty->print_cr(" from-card cache hit."); } assert(contains_reference(from), "We just added it!"); return; } // Note that this may be a continued H region. HeapRegion* from_hr = _g1h->heap_region_containing_raw(from); RegionIdx_t from_hrm_ind = (RegionIdx_t) from_hr->hrm_index(); // If the region is already coarsened, return. if (_coarse_map.at(from_hrm_ind)) { if (G1TraceHeapRegionRememberedSet) { gclog_or_tty->print_cr(" coarse map hit."); } assert(contains_reference(from), "We just added it!"); return; } // Otherwise find a per-region table to add it to. size_t ind = from_hrm_ind & _mod_max_fine_entries_mask; PerRegionTable* prt = find_region_table(ind, from_hr); if (prt == NULL) { MutexLockerEx x(_m, Mutex::_no_safepoint_check_flag); // Confirm that it's really not there... prt = find_region_table(ind, from_hr); if (prt == NULL) { uintptr_t from_hr_bot_card_index = uintptr_t(from_hr->bottom()) >> CardTableModRefBS::card_shift; CardIdx_t card_index = from_card - from_hr_bot_card_index; assert(0 <= card_index && (size_t)card_index < HeapRegion::CardsPerRegion, "Must be in range."); if (G1HRRSUseSparseTable && _sparse_table.add_card(from_hrm_ind, card_index)) { if (G1RecordHRRSOops) { HeapRegionRemSet::record(hr(), from); if (G1TraceHeapRegionRememberedSet) { gclog_or_tty->print(" Added card " PTR_FORMAT " to region " "[" PTR_FORMAT "...) for ref " PTR_FORMAT ".\n", align_size_down(uintptr_t(from), CardTableModRefBS::card_size), hr()->bottom(), from); } } if (G1TraceHeapRegionRememberedSet) { gclog_or_tty->print_cr(" added card to sparse table."); } assert(contains_reference_locked(from), "We just added it!"); return; } else { if (G1TraceHeapRegionRememberedSet) { gclog_or_tty->print_cr(" [tid %d] sparse table entry " "overflow(f: %d, t: %u)", tid, from_hrm_ind, cur_hrm_ind); } } if (_n_fine_entries == _max_fine_entries) { prt = delete_region_table(); // There is no need to clear the links to the 'all' list here: // prt will be reused immediately, i.e. remain in the 'all' list. prt->init(from_hr, false /* clear_links_to_all_list */); } else { prt = PerRegionTable::alloc(from_hr); link_to_all(prt); } PerRegionTable* first_prt = _fine_grain_regions[ind]; prt->set_collision_list_next(first_prt); _fine_grain_regions[ind] = prt; _n_fine_entries++; if (G1HRRSUseSparseTable) { // Transfer from sparse to fine-grain. SparsePRTEntry *sprt_entry = _sparse_table.get_entry(from_hrm_ind); assert(sprt_entry != NULL, "There should have been an entry"); for (int i = 0; i < SparsePRTEntry::cards_num(); i++) { CardIdx_t c = sprt_entry->card(i); if (c != SparsePRTEntry::NullEntry) { prt->add_card(c); } } // Now we can delete the sparse entry. bool res = _sparse_table.delete_entry(from_hrm_ind); assert(res, "It should have been there."); } } assert(prt != NULL && prt->hr() == from_hr, "consequence"); } // Note that we can't assert "prt->hr() == from_hr", because of the // possibility of concurrent reuse. But see head comment of // OtherRegionsTable for why this is OK. assert(prt != NULL, "Inv"); prt->add_reference(from); if (G1RecordHRRSOops) { HeapRegionRemSet::record(hr(), from); if (G1TraceHeapRegionRememberedSet) { gclog_or_tty->print("Added card " PTR_FORMAT " to region " "[" PTR_FORMAT "...) for ref " PTR_FORMAT ".\n", align_size_down(uintptr_t(from), CardTableModRefBS::card_size), hr()->bottom(), from); } } assert(contains_reference(from), "We just added it!"); }
代码真的多,不过我们可以看重点看红色部分。 int from_card 拿到card的index。_sparse_table 保存的是region的index作为key,card的index作为value
HeapRegion* from_hr = _g1h->heap_region_containing_raw(from);
RegionIdx_t from_hrm_ind = (RegionIdx_t) from_hr->hrm_index();
还有 就是 record方法
void HeapRegionRemSet::record(HeapRegion* hr, OopOrNarrowOopStar f) { if (_recorded_oops == NULL) { assert(_n_recorded == 0 && _recorded_cards == NULL && _recorded_regions == NULL, "Inv"); _recorded_oops = NEW_C_HEAP_ARRAY(OopOrNarrowOopStar, MaxRecorded, mtGC); _recorded_cards = NEW_C_HEAP_ARRAY(HeapWord*, MaxRecorded, mtGC); _recorded_regions = NEW_C_HEAP_ARRAY(HeapRegion*, MaxRecorded, mtGC); } if (_n_recorded == MaxRecorded) { gclog_or_tty->print_cr("Filled up 'recorded' (%d).", MaxRecorded); } else { _recorded_cards[_n_recorded] = (HeapWord*)align_size_down(uintptr_t(f), CardTableModRefBS::card_size); _recorded_oops[_n_recorded] = f; _recorded_regions[_n_recorded] = hr; _n_recorded++; } }
看看不看不知道,一看吓一跳啊。之前看过太多的文章说RSET是一个map结构,key是对方的region指针,value是card的index。这么说其实不能算错,但是你看源码实际是通过两个数组实现的。
上面的话我说错了,的确也是有map结构的,_sparse_table.add_card(from_hrm_ind, card_index))。不过为啥还要在record再保存一遍。
现在我们是彻底知道RSET的存储结构了,这样对RSET的理解就会更深了。
RSet有什么好处?
进行垃圾回收时,如果Region1有根对象A引用了Region2的对象B,显然对象B是活的,如果没有Rset,就需要扫描整个Region1或者其它Region,才能确定对象B是活跃的,有了Rset可以避免对整个堆进行扫描。RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。
RSet有什么风险?
通过对RSet实现过程的研究,我们得知应用线程只负责把更新字段所在的Card插入到dirty card queue中,然后由后台线程refinement threads负责RSet的更新操作,如果应用线程插入速度过快,refinement threads来不及处理,那么应用线程将接管RSet更新的任务,这是必须要避免的。
refinement threads线程数量可以通过-XX:G1ConcRefinementThreads或-XX:ParallelGCThreads参数设置
不知道大家注意到没有,无论是SATB还是RSET都有一个缓存结构,SATB会缓存旧的引用,RSET缓存指向新引用的card的地址。而且还有一个共同的特点,就是这个缓存结构都是在当前的线程里的,
之前我一直理解是这两个queue是在region里的,其实是当前的应用线程里的。
浙公网安备 33010602011771号