Panda白话 - G1垃圾收集器 - 三色标记法、漏标、增量更新、SATB
上文讲到G1在并发标记过程中,通过三色标记法来标记存活对象, 三色标记法存在漏标问题,会影响程序正确性, G1采用SATB方案解决漏标问题,本文我们就来重点分析一下SATB
SATB - Snapshot At The Beginning 初始快照
- 由Taiichi Yuasa开发的一个算法 - 首先它是一种思想
- 主要用于GC的并发标记阶段
- 记录并发标记是mutator修改的引用记录,remark阶段(STW)无需全量重新扫描标记
- 快照是以BitMap(位图)的方式实现,BitMap存放对象存活标记,1 为黑色 即存活
见上图、Region包含了5个指针:
- Bottom - 总是指向Region的起始位置
- Previous TAMS - 指向上一次并发处理后的地址
- Next TAMS - 指向并发标记开始之前内存已经分配成功的地址
- Yop - 指向当前内存分配成功的地址
- End - 总是指向Region的终点位置
TAMS - top-at-mark-start
对象在Region中是连续分配的
可以知道:
- [Bottom,End]区间就是Region内存空间大小
- [Bottom,Prev]区间就是上次并发标记结束后,已经标记过的对象内存
- [Prev,Next]区间就是本次并发标记标记到的对象内存
- [Next,Top]区间就是并发标记过程中Mutator新增的对象分配的内存地址
- [Top,End] 区间就是Region还未分配的空闲内存
由上可知,Prev和Next指针解决了并发标记中内存区域问题,并发标记引入了两个**数据结构
**来记录内存标记状态,
- PrevBitMap - 记录Prev指针之前内存标记情况、即【Bottom,Prev】区间
- NextBitMap - 记录Next指针之前内存标记情况、即【Bottom,Next】区间
图解两次并发标记过程:
第一次并发标记开始前 :
- PrevBitMap 为空,NextBitMap 待标记
第一次标记结束后:
- NextBitMap 标记了分区对象存活情况
- NextBitMap位图中黑色区域表示对应对象存活
- 并发标记过程中Mutator继续运行,产生新的对象,Top指针继续增长
第二次并发标记开始前:
- 重置指针,Prev指针指向Next指针位置
- 交换位图,PrevBitMap获取NextBitMap记录,NextBitMap 清空
第二次并发标记结束后:
- Next指针指向标记前已分配内存顶部,即Top指针位置,即完成上次标记时新增对象【Next,Top】区间的扫描标记
- NextBitMap记录所有已扫描对象内存标记状态
- Top指针持续增长
最终标记-Remark开始之前:
同上
最终标记-Remark结束之后:
- Remark阶段STW,Mutator暂停执行,Top不会继续增长
- Prev指向Next
- Next 和Top都指向了已分配对象顶部
- NextBitMap 记录所有对象标记情况
上面步骤可以看出: - 每次并发标记后,将本次标记结果【Bottom,Prev】区间做了一次Snapshot快照,以BitMap位图存储,所有垃圾对象通过快照被识别出来
- 并发标记中Mutator新增的对象都认为是存活对象,设置为灰色,因为SATB关注的是引用的删除,将
o1.filed = new O2()
修改为o1.field = null
这种,会将O2对象置为灰色,加入操作栈,重新进行扫描,解决漏标问题
写屏障:
SATB记录的是删除引用的对象,通过写屏障来实现
写屏障的代码在 oop.inline.hpp
template <class T> inline void oop_store(T* p, oop v) {
if (always_do_update_barrier) {
oop_store((volatile T*)p, v);
} else {
//引用赋值前屏障
update_barrier_set_pre(p, v);
//引用赋值
oopDesc::encode_store_heap_oop(p, v);
//引用赋值后屏障
update_barrier_set((void*)p, v, false /* release */);
}
}
写屏障总结为:
JVM ----> Insert Pre-write barrier // 插入写前屏障
Object.Field = other_object; //引用赋值
JVM ----> Insert Post-write barrier //插入写后屏障
G1 使用的是写前屏障Pre-write barrier
,关注引用的删除,
写前屏障最终执行的代码在:
void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
if (!JavaThread::satb_mark_queue_set().is_active()) return;
Thread* thr = Thread::current();
if (thr->is_Java_thread()) {
JavaThread* jt = (JavaThread*)thr;
//将旧引用保存到satb_mark_queue队列
jt->satb_mark_queue().enqueue(pre_val);
} else {
//本地代码则放入全局共享队列,需上锁
MutexLockerEx x(Shared_SATB_Q_lock,Mutex::_no_safepoint_check_flag);
JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
}
}
分析:
- 将旧引用保存到satb_mark_queue
- 下次并发标记会以此处理satb_mark_queue中的对象
- 失去引用的对象在本轮GC是存活的
- 白色对象会成为浮动垃圾-float garbage