1. 简介
记录集Remembered Sets简称RSet,用于记录对象在不同分区之间的引用关系,目的是为了加速垃圾回收的速度,主要是加速标记阶段。
本文将详细介绍RSet的结构。
通常的,有两种记录引用关系的方式,PointOut和PointIn。
如果obj1.field1=obj2,如果是PointOut方式,则在obj1所在region的RSet记录obj2的位置;如果是PointIn方式,则在obj2所在region记录obj1的位置。G1采用的是PointIn方式。
一共有五种分区间的引用关系:
- 分区内引用
- 新生代分区Y1引用新生代分区Y2
- 新生代分区Y1引用老年代分区O1
- 老年代分区O1引用新生代分区Y1
- 老年代分区O1引用老年代分区O2
YGC时,GC root主要是两类:栈空间和老年代分区到新生代分区的引用关系。
Mixed GC时,由于仅回收部分老年代分区,老年代分区之间的引用关系也将被使用。
因此,我们仅需要记录两种引用关系:老年代分区引用新生代分区,老年代分区之间的引用。
2. RSet
由于PointIn模式的缺点,一个对象可能被引用的次数不固定,为了节约空间,G1采用了三级数据结构来存储:
- 稀疏表:通过哈希表来存储,key是region index,value是card数组
- 细粒度PerRegionTable:当稀疏表指定region的card数量超过阈值时,则在细粒度PRT中创建一个对应的PerRegionTable对象,其包含一个C heap位图,每一位对应一个card
- 粗粒度位图:当细粒度PRT size超过阈值时,则退化为分区位图,每一位表示对应分区有引用到当前分区
每个HeapRegion都包含了一个HeapRegionRemSet,每个HeapRegionRemSet都包含了一个OtherRegionsTable,引用数据就保存在这个OtherRegionsTable中。
我们通过添加引用来了解RSet的结构。
添加引用的入口在heapRegionRemSet.cpp中
void OtherRegionsTable::add_reference(OopOrNarrowOopStar from, uint tid) {
// Note that this may be a continued H region.
HeapRegion* from_hr = _g1h->heap_region_containing(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)) {
assert(contains_reference(from), "We just found " PTR_FORMAT " in the Coarse table", p2i(from));
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) {
CardIdx_t card_index = card_within_region(from, from_hr);
if (_sparse_table.add_card(from_hrm_ind, card_index)) {
assert(contains_reference_locked(from), "We just added " PTR_FORMAT " to the Sparse table", p2i(from));
return;
}
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);
// The assignment into _fine_grain_regions allows the prt to
// start being used concurrently. In addition to
// collision_list_next which must be visible (else concurrent
// parsing of the list, if any, may fail to see other entries),
// the content of the prt must be visible (else for instance
// some mark bits may not yet seem cleared or a 'later' update
// performed by a concurrent thread could be undone when the
// zeroing becomes visible). This requires store ordering.
OrderAccess::release_store(&_fine_grain_regions[ind], prt);
_n_fine_entries++;
// 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 < sprt_entry->num_valid_cards(); i++) {
CardIdx_t c = sprt_entry->card(i);
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);
assert(contains_reference(from), "We just added " PTR_FORMAT " to the PRT (%d)", p2i(from), prt->contains_reference(from));
}
- 大对象可能跨region,因此需要找到该对象的头部region
- 判断region index是否在粗粒度位图中,如是,则直接返回
- 在细粒度PRT中查找region index对应的记录,如有,则返回
- 在稀疏表中添加该region index和card,如果返回成功或found,则返回
- 如果_sparse_table.add_card返回overflow,则表示稀疏表对应region记录已超过阈值,则在细粒度PRT中添加region index和card
稀疏表主要逻辑在sparsePRT.hpp中
class SparsePRTEntry: public CHeapObj<mtGC> {
private:
// The type of a card entry.
typedef uint16_t card_elem_t;
static const size_t card_array_alignment = sizeof(int) / sizeof(card_elem_t);
RegionIdx_t _region_ind;
int _next_index;
int _next_null;
card_elem_t _cards[card_array_alignment];
}
bool RSHashTable::add_card(RegionIdx_t region_ind, CardIdx_t card_index) {
SparsePRTEntry* e = entry_for_region_ind_create(region_ind);
assert(e != NULL && e->r_ind() == region_ind,
"Postcondition of call above.");
SparsePRTEntry::AddCardResult res = e->add_card(card_index);
if (res == SparsePRTEntry::added) _occupied_cards++;
assert(e->num_valid_cards() > 0, "Postcondition");
return res != SparsePRTEntry::overflow;
}
SparsePRTEntry::AddCardResult SparsePRTEntry::add_card(CardIdx_t card_index) {
for (int i = 0; i < num_valid_cards(); i++) {
if (card(i) == card_index) {
return found;
}
}
if (num_valid_cards() < cards_num() - 1) {
_cards[_next_null] = (card_elem_t)card_index;
_next_null++;
return added;
}
// Otherwise, we're full.
return overflow;
}
往稀疏表中添加引用的逻辑主要在两个add_card方法中:
- 根据region index从哈希表中找到SparsePRTEntry
- 在_cards中遍历,如果card已经记录,则返回found;
- 如果小于阈值,则添加card到_cards数组
- 如果大于等于阈值,则返回overflow
细粒度PRT主要逻辑在heapRegionRemSet.cpp的PerRegionTable类中
class PerRegionTable: public CHeapObj<mtGC> {
friend class OtherRegionsTable;
friend class HeapRegionRemSetIterator;
HeapRegion* _hr;
CHeapBitMap _bm;
jint _occupied;
PerRegionTable* _next;
PerRegionTable* _prev;
PerRegionTable * _collision_list_next;
static PerRegionTable* volatile _free_list;
void add_card_work(CardIdx_t from_card, bool par) {
if (!_bm.at(from_card)) {
if (par) {
if (_bm.par_at_put(from_card, 1)) {
Atomic::inc(&_occupied);
}
} else {
_bm.at_put(from_card, 1);
_occupied++;
}
}
}
}
- _bm是个C heap位图,每一位对应region中的一个card
- 添加引用时,先根据card在_bm中找到对应bit位置,然后将该bit置为1
- _occupied引用数量加1,如果是并行操作则使用原子指令加1
粗粒度位图的逻辑简单很多,主要是在OtherRegionsTable中定义了一个CHeapBitMap
class OtherRegionsTable {
CHeapBitMap _coarse_map;
}
3. 总结
在串行和并行GC中,GC通过整堆扫描,来确定对象是否处于可达路径中。
然而G1为了避免整堆扫描,为每个分区记录了一个RSet,记录引用分区内对象的card索引。这样标记时,仅仅需要扫描对应分区的对应card中的对象是否可达即可,极大的提升了GC效率。
4. 引用
jdk12源代码[https://hg.openjdk.java.net/jdk/jdk12]