Snapshot-At-The-Beginning
-
标记方式有两种方式
- 增量更新(Increment Update)
- 初始快照( Snapshot At The Beginning SATB)
-
SATB( Snapshot At The Beginning, 初始快照) 是一种将并发标记阶段开始时对象间的引用关系, 以逻辑快照的形式进行保存的手段
-
介绍并发标记
-
介绍简单标记
- 在简单标记中, 所有可从根直接触达的对象都会被添加标记。 带标记的是存活对象, 不带标记的是死亡对象
- 在简单标记中, 所有可从根直接触达的对象都会被添加标记。 带标记的是存活对象, 不带标记的是死亡对象
-
标记位图
-
将用于标记的比特值等信息单独拿出来放到其他地方, 用来匹配对应的对象。
-
- bottom 表示区域内众多对象的末尾
- nextTAMS 中的 TAMS 是“Top At Marking Start”(标记开始时的 top) 的缩写
- nextTAMS 保存了本次标记开始时的 top, 而 prevTAMS 保存了上次标记开始时的 top。
- next 是本次标记的标 记位图
- prev 是上次标记的标记位图, 保存了上次标记的结果
-
-
-
流程
-
初始标记阶段
- GC 线程首先会创建标记位图 next
- nextTAMS 指的 就是标记开始时 top 所在的位置, 所以在这里我们将它和 top 对齐
-
并发标记阶段
-
概念:在并发标记阶段, GC 线程继续扫描在初始标记阶段被标记过的对象, 完成对大部分存活对象的标记
-
这个阶段是与用户线程并发执行的,那怎么来标记呢
-
首先根据快照,来标记各个引用。
-
new 新对象
-
对象成员变量的变化
-
记录引用关系,所以需要写屏障技术,称之为 SATB 专用写屏障。
-
伪代码
-
1: def satb_write_barrier(field, newobj): 2: if $gc_phase == GC_CONCURRENT_MARK: 3: oldobj = *field // (a) 4: if oldobj != Null: 5: enqueue($current_thread.stab_local_queue, oldobj) // (b) 6: 7: *field = newobj // (c)
-
加入队列后的处理
- 只扫描未被标记的
- 已经标记的不处理
-
并发标记阶段结束后区域的状态
-
疑问
- 多线程环境下如何加入队列
- 由GC线程来处理
- 绑定线程队列
- 线程本地队列满了,再放入全局队列
- 并发写一个字段,如果obj3对象field引用了objo, 两个线程并发赋值obj1,obj2. 导致obj1没有加入队列,那obj1会丢失吗
-
obj1有两种可能,一种是new的,一种是另一个引用。
-
obj1 未被 SATB 专用写屏障获知时对象之间的关系
-
那obj4移除对obj1的引用会有问题吗
-
- 多线程环境下如何加入队列
-
-
最终标记阶段
- 未装满的 SATB 本地队列
-
存活对象计数
- 这个步骤会扫描各个区域的标记位图 next, 统计区域内存活对象的字节数
-
收尾工作
- next_marked_bytes 替换为prev_marked_bytes,同时, prevTAMS 被移到了 nextTAMS 先前的位置
-
- 总的流程图
-
remembered set
- SATB 队列集合主要用 来记录标记过程中对象之间引用关系的变化
- 转移专用记忆集合则用
来记录区域之间的引用关系。 通过使用转移专用记忆集合, 在转移时即
使不扫描所有区域内的对象, 也可以查到待转移对象所在区域内的对象
被其他区域引用的情况, 从而简化单个区域的转移处理 - 那为什么要使用remembered set,什么情况下使用
-
减少全扫描
-
分代垃圾回收,之间是怎么引用的
- 分区内部引用
- 无论是新生代还是老年代的分区内部的引用,都不需要记录引用关系。因为是针对一个分区进行的垃圾回收,要么这个分区被回收,要么不被回收。
- 年轻代与年轻代之间的引用
- G1 的三种回收算法(YGC/MIXED GC/FULL GC)都会全量处理新生代分区,所以新生代都会被遍历到。因此无需记录这种引用关系。
- 年轻代引用年老代
- 无需记录。G1 的 YGC 回收新生代,无需这个引用关系。混合 GC 时,G1 会采用新生代分区作为根,那么在遍历新生代分区时就能找到老年代分区了,无需这个引用关系。对于 FGC 来说,所有分区都会被处理,也无需这个引用关系。
- 年老代引用年轻代
- 需要记录。YGC 在回收新生代时,如果新生代的对象被老年代引用,那么需要标记为存活对象。即此时的根对象有两种,一个是栈空间 / 全局变量的引用,一个是老年代到新生代的引用。
- 年老代引用年老代
- 需要记录。混合 GC 时,只会回收部分老年代,被回收的老年代需要正确的标记哪些对象存活。
- 分区内部引用
-
记录引用方式
- point -in
- point-out
-
如何记录引用关系
- 对象与对象的引用
- region 与 region
- 对象与region
- region 与 卡表(Card Table)
-
Card Table
-
卡表是由元素大小为 1 B 的数组实现的(图 3.3) 。 卡表里的元素称为卡片
-
-
卡表的实体是数组。 数组的元素是 1 B 的卡片, 对应了堆中的 512 B。 脏卡片用灰色表示, 净卡
片用白色表示 -
根据对象获取对应的卡表
- 卡表的实体是数组。 数组的元素是 1 B 的卡片, 对应了堆中的 512 B。
- (对象的地址 - 堆的头部地址)/ 512
-
因为卡片的大小是 1 B, 所以可以用来表示很多状态。 卡片的种类很多, 我们主要关注以下两种
- 净卡片
- 净卡片
-
-
记忆集合的构造
- 每个区域中都有一个转移专用记忆集合, 它是通过散列表实现的。 散列
表的键是引用本区域的其他区域的地址, 而散列表的值是一个数组, 数
组的元素是引用方的对象所对应的卡片索引。
- 每个区域中都有一个转移专用记忆集合, 它是通过散列表实现的。 散列
-
写屏障
-
当对象的域被修改时, 被修改对象所对应的卡片会被转移专用写屏障记
录到转移专用记忆集合中。 转移专用写屏障的伪代码如代码清单 -
-
多线程优化
-
-
-
记忆集合维护线程
- 转移专用记忆集合维护线程主要进行下列处理
- 从转移专用记忆集合日志的集合中取出转移专用记忆集合日志, 从
头开始扫描 - 将卡片变为净卡片
- 检查卡片所对应存储空间内所有对象的域
- 往域中地址所指向的区域的记忆集合中添加卡片
- 从转移专用记忆集合日志的集合中取出转移专用记忆集合日志, 从
- 转移专用记忆集合维护线程主要进行下列处理
-
热卡片
- 频繁发生修改的存储空间所对应的卡片称为热卡片(hot card)
- 热卡
片可能会多次被转移专用记忆集合维护线程处理成脏卡片, 从而加重转
移专用记忆集合维护线程的负担, 因此需要特别处理。 - 卡片计数表,它记录了卡片变成脏卡片的次
数。 - 脏卡片阈值(默认是 4)
-
那GC时,记忆集合如何转移呢
-
转移对象
- 是指参考并发标记提供的信息来选择被转移的区域。 被选中的区域称
为回收集合 - 是指将回收集合内由根直接引用的对象, 以及被其他区域引用的对象
转移到空闲区域中。 - 是指以②中转移的对象为起点扫描其子孙对象, 将所有存活对象一并
转移。 当③结束之后, 回收集合内的所有存活对象就转移完成了。 - 伪代码