Hotspot源码分析(G1)——write_barrier
前言
本文将针对 G1GC 算法篇中提到的 write barrier 技术基于源码进行分析,如有偏差,请指正。
G1 SATB 与 Rset维护都会调用到 write-barrier ( SATB 采用 pre write-barrier 完成,维护 RSet 采用 post write-barrier 完成) mutator 在 write-barrier 中仅仅将要做的事推送到队列中,然后通过另外的线程取出队列中的信息批量完成剩余的动作。
本文将分析 write-barrier 在OpenJDK8源码中解释器与C2编译中的实现。
write-barrier 在模板解释器的实现
在 G1GC 算法篇介绍两种 write-barrier 时提过, SATB 写屏障的目的是当并发标记时避免漏标
,而维护的 Rset 记录的则是跨界引用
。漏标的产生
和跨界引用的修改
根本原因都与对象的域被修改有关。查看如下例子:
1 |
|
例子转换成字节码的核心部分如下,字节码的阅读不作详细解答,可查阅相关文档
1 |
|
当解释执行时,会通过字节码 putfield 实现步骤2 ,所以来看一下 putfield 在模板解释器中的实现:
1 |
|
事实上字节码 aastore 字节码也会调用到 do_oop_store 函数,本文仅用 putfield 举例, do_oop_store 函数会将 oop (或 NULL )存储在 obj 描述的地址
1 |
|
排除压缩指针与其他选项匹配的 barrier 代码,不难看出,当开启 G1GC 时,会在 store_heap_oop(_null) 之前调用 g1_write_barrier_pre ,之后调用 g1_write_barrier_post 函数。
g1_write_barrier_pre 在解释运行中的实现
回顾 G1GC 算法实现,SATB 专用写屏障的伪代码如下所示:
1 |
|
在伪代码中不难看出 SATB 专用写屏障,通过在将新对象写入域之前记录原来域对象,并滞后标记的方式来避免漏标。
在 G1GC 算法篇介绍 SATB 时提过,首先会将 old 对象放到本地线程的 SATB 队列中,所以首先来看一下 JavaThread 类的以下两个属性
1 |
|
_satb_mark_queue 与 _dirty_card_queue 属于线程私有,当线程本地队列满了之后,会分别提交到全局的 _satb_mark_queue_set 与 _dirty_card_queue_set 交给 G1 线程批量处理。
ObjPtrQueue 的父类 class PtrQueue 具有如下属性与函数
1 |
|
注释很好理解,三个属性分别是是否活跃、buffer 地址与最后一个对象的末尾索引。其中 _index 属性为 0 时代表队列满了,具体处理在后文分析。
接下来看一下 g1_write_barrier_pre 的核心实现
1 |
|
不难发现,整体实现与伪代码类似,完成了对前值的入队。
g1_write_barrier_post 在解释运行中的实现
g1_write_barrier_post 的伪代码如下
1 |
|
过滤 obj 和 newobj 位于同一个区域,或者 newobj 为 Null 的时候, is_dirty_card() 用来检查参数 obj 所对应的卡片是否
为脏卡片。
1 |
|
整体逻辑与伪代码类似, 源码的注释已经能够说明过程了,此处不作赘述。
write-barrier 在C2编译的实现
依旧是如下例子:
1 |
|
当触发 C2 编译时,首先根据字节码创建理想图
1 |
|
创建详细过程可以读者自行理解,此处只关注调用关系,最终调用到 store_oop
1 |
|
在 store_to_memory 函数的前后分别加入了 pre_barrier 和 post_barrier 当函数需要 C2 编译运行,同时 GC 选用的是 G1GC 时,会在解析字节码时就为 write_barrier 生成相关的结点。
pre_barrier 在C2编译中的实现
接下来追溯 pre_barrier 到底生成了哪些节点,
1 |
|
整体逻辑类似,判断 pre_val 是否空,线程队列是否满,根据判断结果调用运行时或者生成将 pre_val 入栈的节点
在生成理想图节点后,C2编译器会对理想图做优化,最后生成机器代码,详细过程如下
1 |
|
这部分不是本文关注的重点,为了方便研究 C2 编译结果可以通过 slowdebug 版本的 hotspot ,在运行程序时添加参数 -XX:+PrintOptoAssembly 查看 Opto (也可以直接查看汇编,但是 Opto 更直观),仍以上文例子为例可以在x86平台得到如下两段 Opto 节选,分别是 G1GC 与其他不会产生 pre_barrier 的 GC
1 |
|
1 |
|
整体过程与理想图阶段生成的节点,能一一对应。
post_barrier 在C2编译中的实现
post_barrier 的逻辑大体与 pre_barrier 相同,如下会根据 barrier 类型 G1GC 匹配到 g1_write_barrier_post
1 |
|
理想图部分代码与模板解释器体现的逻辑也是一致的,接下来看看最终生成的 Opto
1 |
|
write-barrier 在运行时的实现
运行时,不仅需要处理队列满后加入队列的对象,还需要将满的队列放入全局链表,并重置线程私有队列。上文频繁提及,当队列满了会调用运行时实现写屏障,接下来看一下运行时对写屏障的实现。
g1_wb_pre 与 g1_wb_post 在运行时的实现
1 |
|
运行时的代码更直观,即将域前值/卡片地址入队,其中
1 |
|
这些值在上文曾多次提及
根据上面对模板解释器和 C2 编译的分析,只有当当前线程队列满时才会调用到运行时,追溯 enqueue 的实现,
1 |
|
enqueue 函数的实现在 ObjPtrQueue 与 DirtyCardQueue 类的共同父类 PtrQueue 中
1 |
|
参考
jdk8源代码