卡表
在G1
堆中,卡表是由1
字节元素组成的数组,数组里面的元素称为卡页,这个卡表会映射整个堆空间,每个卡页会对应堆中的512
字节的空间。如下图,大小为1M
的region
会对应2048
个卡表,1GB
的堆会存在2097152
个卡表。
Rset
Rset
用来记录跨对象间跨Region
的引用,当虚拟机进行垃圾回收时,通过扫描Rset
来判断被回收的对象是否被其他Region
内的对象引用。
什么时候需要记录引用关系
不是所有的对象被其他对象引用都需要记录,如下是引用关系的归纳:
- 同
Region
之间的对象引用 - 新生代对象引用老年代对象
- 新生代不同
Region
下的对象间发生引用关系 - 老年代对象引用新生代对象
- 老年代不同
Region
下的对象间发生引用
同一个Region
中的对象如果发生引用,不管是老年代还是新生代,都不需要在Rset
中记录引用关系,因为当一个Region
被回收时,会扫描整个Region
。
新生代引用老年代对象时不需要记录引用关系。老年代进行垃圾回收前会进行一次年轻代的垃圾回收,并且将新生代存活的对象作为GC ROOT
进行后续的标记。
非同一个Region
的新生代对象间发生引用不需要记录到Rset
。新生代垃圾回收采用的复制算法,JVM
会扫描整个新生代堆区,然后把存活的对象复制到新生代存活区或者对象晋升到老年代。
老年代引用新生代对象时需要将引用关系记录到Rset
中。新生代进行垃圾回收时,需要确认新生代的对象有没有被老年代的对象引用,通过Rset
记录引用关系来避免扫描整个老年代堆区。
非同一个Region
的老年代对象间发生引用时需要将引用关系记录到Rset
中。老年代的回收不是全量的,每一次混合回收只是回收部分垃圾占比高的老年代Region
堆,Rset
避免扫描整个老年代堆区。
Point In Or Point Out
对象分配在Region
中,而每个Region
都会对应一个Rset
,假设 A、B
分别为老年代、新生代对象,对象A
引用对象B
,当将引用记录到A
所对应的Rset
中时,是一种Point Out
方式,即我引用了谁,另一种方式Point In
,谁引用了我。
新生代发生垃圾回收时,如果采用Point Out
方式,那么需要扫描整个Rset
来确认新生代对象是否被老年代对象引用,这种方式效率太低,而Point In
方式,只需要扫描被回收对象所对应的Rset
,就可以知道哪些对象引用了自己。显然G1
采用了Point In
方式。
Rset 数据结构
Rset
是一个虚拟的概念,当一个对象被其他对象引用时,会通过一个Per Region Table
来记录引用方的Region
索引和卡表索引,并且随着Region
引用次数变化,PRT
的存储结构也会出现变化,如下是Per Region Table
的三种数据结构:
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/heapRegionRemSet.hpp
class OtherRegionsTable VALUE_OBJ_CLASS_SPEC {
//粗粒度PRT
BitMap _coarse_map;
//细粒度PRT
PerRegionTable** _fine_grain_regions;
//稀疏PRT
SparsePRT _sparse_table;
}
SparsePRT
SparsePRT
是稀疏PRT
,它的数据结构是哈希表,key
是引用方的Region Index
,value
是对象所在的卡页。
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/sparsePRT.hpp
class RSHashTable : public CHeapObj<mtGC> {
//哈希槽,它的索引=RegionIndex&capacity,value是_entries数组的索引,通过value可以获取_entries数组中的SparsePRTEntry对象
int* _buckets;
//_entries是SparsePRTEntry类型的数组,每一个槽位存储一个SparsePRTEntry对象
SparsePRTEntry* _entries;
}
class SparsePRTEntry: public CHeapObj<mtGC> {
//key
RegionIdx_t _region_ind;
//解决哈希冲突,指向下一个SparsePRTEntry对象
int _next_index;
//value 卡表索引,默认大小是4
CardIdx_t _cards[1];
}
举一个例子,假设A、B、C
分别位于RegionA RegionB RegionC
,Region
堆被卡表映射,对象A
在卡页0
中,对象B
在卡页4
中,对象C
在卡页8
中,当对象B、C
引用A
时,RegionA
对应的Rset
会记录B、C
所在的Region
索引。
如下,Rset
添加引用记录时,首先会在_entries
数组中申请一个空间来存储SparsePRTEntry
对象,然后将SparsePRTEntry
对象的_cards
来存储引用方的卡页,最后将SparsePRTEntry
对象在_entries
数组中的索引存储到_buckets[RegionIndex&capacity]
处。
如下是SparsePRT
添加引用的代码:
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/sparsePRT.cpp
//region_ind region索引,card_index 卡表索引
bool RSHashTable::add_card(RegionIdx_t region_ind, CardIdx_t card_index) {
//生成SparsePRTEntry对象
//SparsePRTEntry对象的_region_ind变量会存储RegionIndex
SparsePRTEntry* e = entry_for_region_ind_create(region_ind);
//将引用方的卡页保存到SparsePRTEntry对象的_cards数组中
SparsePRTEntry::AddCardResult res = e->add_card(card_index);
//记录卡页数量
if (res == SparsePRTEntry::added) _occupied_cards++;
return res != SparsePRTEntry::overflow;
}
创建SparsePRTEntry对象
稀疏PRT
在初始化时会创建一组连续的空间_entries
,它里面存储的是SparsePRTEntry
指针,通过该指针可以对SparsePRTEntry
对象进行操作。
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/sparsePRT.cpp
//创建SparsePRTEntry对象
SparsePRTEntry*
RSHashTable::entry_for_region_ind_create(RegionIdx_t region_ind) {
//检查 region_ind是否保存过
SparsePRTEntry* res = entry_for_region_ind(region_ind);
if (res == NULL) {
//申请空间,返回_entries数组的索引
int new_ind = alloc_entry();
//通过_entries的索引可以获取到SparsePRTEntry指针
res = entry(new_ind);
//通过指针对SparsePRTEntry对象进行初始化
res->init(region_ind);
//生成哈希槽_bucketsd的索引
int ind = (int) (region_ind & capacity_mask());
//如果有哈希冲突通过链表来解决
res->set_next_index(_buckets[ind]);
//将链表头节点存储到数组ind处
_buckets[ind] = new_ind;
//计数
_occupied_entries++;
}
return res;
}
保存卡页
初始化SparsePRTEntry
对象后将引用对象所在的卡页存储到SparsePRTEntry
的_cards
数组中,该数组默认大小是4
,如果数组满了之后,JVM
会将稀疏PRT
升级未细粒度PRT
。
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/sparsePRT.cpp
SparsePRTEntry::AddCardResult SparsePRTEntry::add_card(CardIdx_t card_index) {
//将Card Index 保存到_cards数组中
//cards_num()返回数组的大小,它的默认值是4
CardIdx_t c;
for (int i = 0; i < cards_num(); i += UnrollFactor) {
c = _cards[i];
//c == card_index 说明卡表已经被标记
if (c == card_index) return found;
//c == NullEntry 说明有空余的空间来标记引用方所在的卡表
if (c == NullEntry) { _cards[i] = card_index; return added; }
c = _cards[i + 1];
if (c == card_index) return found;
if (c == NullEntry) { _cards[i + 1] = card_index; return added; }
c = _cards[i + 2];
if (c == card_index) return found;
if (c == NullEntry) { _cards[i + 2] = card_index; return added; }
c = _cards[i + 3];
if (c == card_index) return found;
if (c == NullEntry) { _cards[i + 3] = card_index; return added; }
}
for (int i = 0; i < cards_num(); i++) {
CardIdx_t c = _cards[i];
if (c == card_index) return found;
if (c == NullEntry) { _cards[i] = card_index; return added; }
}
//_cards 的默认大小是4,如果超过了容量大小,将尝试使用细粒度数据结构
return overflow;
}
细粒度PRT
稀疏PRT
存储的是引用方对象所处的卡页索引,如果引用次数变多会浪费内存空间,所以默认情况下,稀疏PRT
存储某个Region
超过4个卡页就会采用细粒度PRT
,原来的数据迁移到细粒度PRT
。
细粒度PRT
不会存储卡页,而是将卡页对应的BitMap
处设置为1
。
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/heapRegionRemSet.hpp
class OtherRegionsTable VALUE_OBJ_CLASS_SPEC {
//细粒度PRT哈希槽,
PerRegionTable** _fine_grain_regions;
}
class PerRegionTable: public CHeapObj<mtGC> {
//Region
HeapRegion* _hr;
//用来标记引用方的卡表
BitMap _bm;
//解决哈希冲突,同一个槽位使用_collision_list_next连接成链表
PerRegionTable * _collision_list_next;
}
细粒度PRT
的结构图如下:
细粒度PRT保存索引
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/heapRegionRemSet.cpp
void add_card_work(CardIdx_t from_card, bool par) {
//_bm 是一个BitMap,用来记录卡页是否被标记过
if (!_bm.at(from_card)) {
//par 是否是并发环境
if (par) {
//将_bm的槽设置为1,表示某个卡页的引用
if (_bm.par_at_put(from_card, 1)) {
Atomic::inc(&_occupied);
}
} else {
_bm.at_put(from_card, 1);
//用来记录region的引用次数
//delete_region_table方法中会使用到
//清理细粒度PRT时,优先清理_occupied大的PRT,
_occupied++;
}
}
}
粗粒度PRT
粗粒度PRT
的元素由细粒度PRT
升级而来,当细粒度PRT
满了之后,会将引用最多的细粒度PRT
删除,然后将该PRT
持有的Region
保存到全局BitMap
中,整个PRT
的数据结构维度由卡页变成Region
。
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/heapRegionRemSet.cpp
PerRegionTable* OtherRegionsTable::delete_region_table() {
PerRegionTable* max = NULL;
jint max_occ = 0;
PerRegionTable** max_prev;
size_t max_ind;
size_t i = _fine_eviction_start;
for (size_t k = 0; k < _fine_eviction_sample_size; k++) {
size_t ii = i;
//获取细粒度PRT保存的引用
PerRegionTable** prev = &_fine_grain_regions[ii];
PerRegionTable* cur = *prev;
//找出occupied最大的region
while (cur != NULL) {
//Region被引用的次数
jint cur_occ = cur->occupied();
if (max == NULL || cur_occ > max_occ) {
max = cur;
max_prev = prev;
max_ind = i;
max_occ = cur_occ;
}
prev = cur->collision_list_next_addr();
cur = cur->collision_list_next();
}
}
size_t max_hrs_index = (size_t) max->hr()->hrs_index();
//将occupied最大的region是否保存到粗粒度PRT中
if (!_coarse_map.at(max_hrs_index)) {
//将BitMap对应的max_hrs_index设置为true
_coarse_map.at_put(max_hrs_index, true);
_n_coarse_entries++;
}
//返回max,这个maxRegion后续会被初始化掉
return max;
}
Rset 管理引用的完整代码
//https://hg.openjdk.org/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/g1/heapRegionRemSet.cpp
//from 引用来源 tid 线程id
void OtherRegionsTable::add_reference(OopOrNarrowOopStar from, int tid) {
//当前线程使用的heap region索引
size_t cur_hrs_ind = (size_t) hr()->hrs_index();
//获取持有引用的对象所在卡表
int from_card = (int)(uintptr_t(from) >> CardTableModRefBS::card_shift);
if (from_card == _from_card_cache[tid][cur_hrs_ind]) {
return;
} else {
_from_card_cache[tid][cur_hrs_ind] = from_card;
}
//From Region
HeapRegion* from_hr = _g1h->heap_region_containing_raw(from);
//From Region Index
RegionIdx_t from_hrs_ind = (RegionIdx_t) from_hr->hrs_index();
//粗粒度prt _coarse_map 是一个BitMap ,用来标记Region是否被标记
if (_coarse_map.at(from_hrs_ind)) {
return;
}
//程序执行到此,说明Rset没有使用粗粒度的prt来保存引用
size_t ind = from_hrs_ind & _mod_max_fine_entries_mask;
//检查是否使用细粒度的PRT来保存引用
PerRegionTable* prt = find_region_table(ind, from_hr);
if (prt == NULL) {
//存在并发情况,加锁
MutexLockerEx x(&_m, Mutex::_no_safepoint_check_flag);
//双重判断
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;
// 如果允许使用稀疏PRT
if (G1HRRSUseSparseTable &&
//尝试将卡页索引保存到稀疏PRT,key是RegionIndex,value 是CardIndex
_sparse_table.add_card(from_hrs_ind, card_index)) {
return;
}
//如果添加稀疏PRT失败,将使用细粒度PRT保存引用
if (_n_fine_entries == _max_fine_entries) {
//如果细粒度PRT已经满了,将引用最多细粒度PRT删除,然后将其持有的Region保存到粗粒度PRT
prt = delete_region_table();
prt->init(from_hr, false);
} else {
//申请空间创建细粒度PRT
prt = PerRegionTable::alloc(from_hr);
link_to_all(prt);
}
//获取细粒度哈希槽ind处的元素
PerRegionTable* first_prt = _fine_grain_regions[ind];
//处于哈希冲突
prt->set_collision_list_next(first_prt);
_fine_grain_regions[ind] = prt;
//计数
_n_fine_entries++;
if (G1HRRSUseSparseTable) {
//将稀疏PRT里面的数据迁移到细粒度卡表中
SparsePRTEntry *sprt_entry = _sparse_table.get_entry(from_hrs_ind);
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_hrs_ind);
}
}
}
//将引用方的卡页索引保存到细粒度PRT的bitmap中
prt->add_reference(from);
}