前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >if (b) Atomic::inc(&_processed_buffers_mut);

if (b) Atomic::inc(&_processed_buffers_mut);

原创
作者头像
不会飞的小鸟
修改2021-08-03 10:33:31
5510
修改2021-08-03 10:33:31
举报
文章被收录于专栏:只为你下只为你下

笔者在之前讲解g1 youngGC源码的中提到过关于g1写屏障和Rset(记忆集合)等相关知识点,之前限于文章长度(ps:全部介绍完博客会比较长)跳过了这个部分只是简单介绍了下概念,今天我们来继续从源码出发,探究g1的写屏障和记忆集合等相关技术内幕。

一.写屏障(write barrier)

关于写屏障,其实要从垃圾回收的三色标记说起,网上关于三色标记的文章很多,具体说明也比较详细,笔者在这里就不在进行详细说明,本文的重点还是放在源码解析与阅读上。

在三色标记算法中,只有同时满足以下两种条件就会产生漏标的问题:

灰色对象断开了白色对象的引用(直接或间接的引用);即灰色对象原来成员变量的引用发生了变化。

黑色对象重新引用了该白色对象;即黑色对象成员变量增加了新的引用。

我们只要破坏其中一个条件就可以解决这个问题,而解决这个问题就需要用到读屏障和写屏障,在jvm的垃圾回收器中,zgc使用的是读屏障,笔者有篇相关博客专门介绍了zgc的技术内幕而我们现在说的g1则是使用的写屏障,准确的说是SATB+写屏障(cms用的是写屏障+增量更新)。

写屏障是在对象属性引用另一个对象的时候才会触发,我们先写一段这样的java代码:

public class Test {

public static void main(String[] args) {

A a = new A();

B b = new B();

//这里我们将A对象的两个属性以不同方式修改引用

//1.public修饰的b属性直接修改

//2.private修饰的c属性用set方法修改

a.b = b;

a.b = null;

a.setC(b);

a.setC(null);

}

}

public class A {

public B b;

private B c;

public void setC(B c) {

this.c = c;

}

}

public class B {

}

因为java是先编译成.class字节码文件,之后由jvm将字节码逐行进行解释执行(当然弱代码执行的次数达到一定阈值,也会将其编译成机器码,本文重点不在这里,笔者就不过多阐述)

我们将刚才写的代码编译成.class文件,用字节码反编译器查看下字节码:

A.class 的set方法

0 aload_0

1 aload_1

//我们看到这里调用了putfield字节码

2 putfield #2 <B.a : Ljava/lang/String;>

5 return

Test.class 的main方法

0 new #2 <A>

3 dup

4 invokespecial #3 <A.<init> : ()V>

7 astore_1

8 new #4 <B>

11 dup

12 invokespecial #5 <B.<init> : ()V>

15 astore_2

//这里是两个入栈操作,后面我们会讲到

16 aload_1

17 aload_2

//我们看到这里调用了putfield字节码

18 putfield #6 <A.b : LB;>

21 aload_1

22 aconst_null

//我们看到这里调用了putfield字节码

23 putfield #6 <A.b : LB;>

26 aload_1

27 aload_2

28 invokevirtual #7 <A.setC : (LB;)V>

31 aload_1

32 aconst_null

33 invokevirtual #7 <A.setC : (LB;)V>

36 return

由此可见putfield字节码命令就是我们这次查看源码的入口啦!

从jdk的源码中找到putfield的字节码命令,在templateTable.cpp中,这个文件是模板解释器,我们简单介绍下,模板解释器是字节码解释器(早期版本jdk的解释器)的优化,早期字节码解释器是逐条翻译,效率低下现在已经不用了,而模板解释器是将每一条字节码与一个模板函数(主要是汇编)关联,用模板函数直接生成机器码从而提高性能。

我们来看看putfield的定义:

void TemplateTable::initialize() {

......

//def方法是用来创建模板的,我们可以简单理解成会将字节码putfield和putfield模板进行关联

//当碰到putfield字节码,就会调用putfield函数模板

def(Bytecodes::_putfield, ubcp|____|clvm|____, vtos, vtos, putfield,f2_byte);

}

我们直接来看putfield函数模板:

//putfield模板

void TemplateTable::putfield(int byte_no) {

//第二个参数是是否是static属性

putfield_or_static(byte_no, false);

}

//我们看到这个方法里就由很多封装的汇编指令了,我们略过一些汇编指令,来看下写屏障的核心逻辑

void TemplateTable::putfield_or_static(int byte_no, bool is_static) {

......

//获取属性的地址(用对象和属性的偏移量封装成address)

const Address field(obj, off, Address::times_1);

......

// 对象类型

{

//这个方法会出栈一个对象引用,并将其放入rax寄存器(内存寄存器)中

//这里解释下,我们的例子中字节码是这样的

//aload_1

//aload_2

//putfield

//局部变量表中编号1是引用a, 编号2是引用b,都是引用类型,存的都是地址

//在执行aload_2前会把aload_1加载的a引用入栈

//在执行putfield前会把aload_2加载的b引用入栈

//所以这里第一次出栈是b的引用

__ pop(atos);

//第二次出栈是a的引用

if (!is_static) pop_and_check_object(obj);

//存储对象的方法,我们进去看下

do_oop_store(_masm, field, rax, _bs->kind(), false);

if (!is_static) www.wanjiashidai.com{

patch_bytecode(Bytecodes::_fast_aputfield, bc, rbx, true, byte_no);

}

//跳到结束

__ jmp(Done);

}

//后面是一些其他基本类型,这里就不进行展开

......

}

//这个方法逻辑还是比较清晰的

//这里注意obj是可以理解为a.b这个引用,后文会统一用obj代替a.b这个引用

//val也是指向B对象的引用

static void do_oop_store(InterpreterMacroAssembler* _masm,

Address obj,

Register val,

BarrierSet::Name barrier,

bool precise) {

//根据屏障类型判断

switch (barrier) {

//g1这里会走这个分支

case BarrierSet::G1SATBCT:

case BarrierSet::G1SATBCTLogging:

{

//这里判断如果obj不是属性,则直接将obj的值传输到rdx寄存器(本案例中不会进入这里)

if (obj.index() == noreg && obj.disp() == 0) {

if (obj.base() != rdx) {

__ movq(rdx, obj.base());

}

} else {

//这里会把传入的a引用地址传输到rdx寄存器

__ leaq(rdx, obj);

}

//写前屏障,主要是SATB处理

//这里的横线__是汇编器的别名,根据不同的系统会调用不同的汇编器

//本文我们只看64位linux的代码

//rdx和rbx都是内存寄存器

//rdx此时已经存储了obj的地址

__ g1_write_barrier_pre(rdx /* obj */,

rbx /* pre_val */,

r15_thread /* thread */,

r8 /* tmp */,

val != noreg /* tosca_live */,

false /* expand_call */);

//如果对象是null则进入这个方法,在a.b上存空值

if (val == noreg) {

__ store_heap_oop_null(Address(rdx, 0));

} else {

......

//把指向b对象的引用存到a.b上

//准确的说是把引用存到本例中A对象的b属性偏移量上

__ store_heap_oop(Address(rdx, 0), val);

//写后屏障

__ g1_write_barrier_post(rdx /* store_adr */,

new_val /* new_val */,

r15_thread /* thread */,

r8 /* tmp */,

rbx /* tmp2 */);

}

}

break;

//非g1会走这个分支,我们就不再展开

case BarrierSet::CardTableModRef:

case BarrierSet::CardTableExtension:

{

if (val == noreg) {

__ store_heap_oop_null(obj);

} else {

__ store_heap_oop(obj, val);

if (!precise || (obj.index() == noreg && obj.disp() == 0)) {

__ store_check(obj.base());

} else {

__ leaq(rdx, obj);

__ store_check(rdx);

}

}

}

break;

......

}

这里涉及到的入栈出栈的知识点是——栈顶缓存,网上有许多关于这方面的文章,有兴趣的读者可以自行了解下,这里就不做过多介绍。

我们看到在引用对象的方法之前和之后都由屏障,类似切面,我们来看看这两个屏障方法:

//找到x86架构的汇编器文件macroAssembler_x86.cpp

//写前屏障方法

void MacroAssembler::g1_write_barrier_pre(Register obj,

Register pre_val,

Register thread,

Register tmp,

bool tosca_live,

bool expand_call) {

//前面很多封装的汇编指令我们忽略,会做一些检测

......

//如果obj不为空,我们就根据obj引用获取其之前引用的对象的地址

if (obj != noreg) {

load_heap_oop(pre_val, Address(obj, 0));

}

//这个命令其实是比较之前的对象是不是空值,如果是空值则不继续执行

cmpptr(pre_val, (int32_t) NULL_WORD);

jcc(Assembler::equal, done);

......

//这里是false

if (expand_call) {

LP64_ONLY( assert(pre_val != c_rarg1, "smashed arg"); )

pass_arg1(this, thread);

pass_arg0(this, pre_val);

MacroAssembler::call_VM_leaf_base(CAST_FROM_FN_PTR(address, SharedRuntime::g1_wb_pre), 2);

} else {

//这里会用汇编指令调用SharedRuntime::g1_wb_pre这个方法

call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::g1_wb_pre), pre_val, thread);

}

......

}

//真正的写前屏障方法,JRT_LEAF可以理解是一个定义方法的宏

JRT_LEAF(void, SharedRuntime::g1_wb_pre(oopDesc* orig, JavaThread *thread))

if (orig == NULL) {

assert(false, "should be optimized out");

return;

}

//将对象的指针加入satb标记队列

thread->satb_mark_queue().enqueue(orig);

JRT_END

//写后屏障方法

void MacroAssembler::g1_write_barrier_post(Register store_addr,

Register new_val,

Register thread,

Register tmp,

Register tmp2) {

#ifdef _LP64

assert(thread == r15_thread, "must be");

#endif // _LP64

Address queue_index(thread, in_bytes(JavaThread::dirty_card_queue_offset() +

PtrQueue::byte_offset_of_index()));

Address buffer(thread, in_bytes(JavaThread::dirty_card_queue_offset() +

PtrQueue::byte_offset_of_buf()));

BarrierSet* bs = Universe::heap()->barrier_set();

CardTableModRefBS* ct = (CardTableModRefBS*)bs;

assert(sizeof(*ct->byte_map_base) == sizeof(jbyte), "adjust this code");

Label done;

Label runtime;

//下面几条命令涉及到汇编逻辑比较,有兴趣的读者可以自行查阅,笔者这里就不进行展开

//判断是否跨regions

//先将引用的地址放到r8寄存器(tmp参数上个方法传入的)中

//再将新对象的地址和r8中的地址进行异或运算,结果存入r8中

//之后将r8的结果逻辑右移LogOfHRGrainBytes位(region大小的log指数+1),并将移出的最后一位加入cf指示器

//最后判断cf中是0还是1即可判断store_addr与new_val两个地址之间是否相差一个region大小

//0即不相差,1即相差

movptr(tmp, store_addr);

xorptr(tmp, new_val);

shrptr(tmp, HeapRegion::LogOfHRGrainBytes);

jcc(Assembler::equal, done);

//判断是否为空

cmpptr(new_val, (int32_t) NULL_WORD);

jcc(Assembler::equal, done);

const Register card_addr = tmp;

const Register cardtable = tmp2;

//将存储的地址赋值给card_addr变量

movptr(card_addr, store_addr);

//将地址逻辑右移card_shift个位,可以理解为计算出其所属card的index

shrptr(card_addr, CardTableModRefBS::card_shift);

//加载卡表数组的基址的偏移量到cardtable

movptr(cardtable, (intptr_t)ct->byte_map_base);

//加上卡表数组的基址偏移量即可算出card在card数组中的有效地址

addptr(card_addr, cardtable);

//判断是否是young区的卡,如果是则不继续执行

cmpb(Address(card_addr, 0), (int)G1SATBCardTableModRefBS::g1_young_card_val());

jcc(Assembler::equal, done);

//判断是否已经是脏卡,如果是则不继续执行

cmpb(Address(card_addr, 0), (int)CardTableModRefBS::dirty_card_val());

jcc(Assembler::equal, done);

//将card赋值脏卡

movb(Address(card_addr, 0), (int)CardTableModRefBS::dirty_card_val());

......

//执行写后屏障方法

call_VM_leaf(CAST_FROM_FN_PTR(address, SharedRuntime::g1_wb_post), card_addr, thread);

......

}

//真正的写后屏障

JRT_LEAF(void, SharedRuntime::g1_wb_post(void* card_addr, JavaThread* thread))

//将card加入dcq队列

thread->dirty_card_queue().enqueue(card_addr);

JRT_END

这里用到的汇编命令比较多,笔者将几步关键步骤进行了标注,如果有兴趣,读者可以自行了解下相关命令,这里就不进行过多讲解。

到这里我们都知道g1修改对象属性引用时会使用的两种写屏障,并且为了提高效率都是先将要处理的数据放到队列中:

1.写前屏障——处理SATB(本质是快照,用于解决并发标记时修改引用可能会造成漏标的问题),将修改前引用的对象的地址加入satb队列,待到gc并发标记的时候处理。(关于写前屏障本文不重点介绍,以后笔者会介绍GC相关的文章中再介绍)

2.写后屏障——找到对应的card标记为dirty_card,加入dirty_card队列

本文我们重点关注下写后屏障,通过上面的源码分析,我们已经看到被修改过引用所处的card都已经被标记为dirty_card,即将卡表数组(本质是字节数组,元素可以理解为是一个标志)中对对应元素进行修改为dirty_card。说到card(卡页),dirty_card(脏卡),我们不得不先从他们的起源card_table(卡表)说起。

二.卡表(card_table)

在写后屏障的源码中有一段关于card计算的汇编代码,可能比较难以理解,笔者在这里画个图来方便解释,通过这张图我们也可以理解卡表,卡页,脏卡的概念:

结合图和我们之前看的写屏障的源码,我们概括下卡表,卡页,脏卡还有写屏障的关系:

卡表(card_table)全局只有一个可以理解为是一个bitmap,并且其中每个元素即是卡页(card)与堆中的512字节内存相互映射,当这512个字节中的引用发生修改时,写屏障就会把这个卡页标记为脏卡(dirty_card)。

接下来我们看看卡表创建的源码:

//卡表相关类的初始化列表

CardTableModRefBS::CardTableModRefBS(MemRegion whole_heap,

int max_covered_regions):

ModRefBarrierSet(max_covered_regions),

_whole_heap(whole_heap),

_guard_index(cards_required(whole_heap.word_size()) - 1),

_last_valid_index(_guard_index - 1),

_page_size(os::vm_page_size()),

_byte_map_size(compute_byte_map_size())

{

.....

//申请一段内存空间,大小为_byte_map_size

//且没有传入映射内存映射的基础地址,即从随机地址映射

//底层会调内核mmap(),这里就不进行展开

ReservedSpace heap_rs(_byte_map_size, rs_align, false);

MemTracker::record_virtual_memory_type((address)heap_rs.base(), mtGC);

...

//赋值给卡表

_byte_map = (jbyte*) heap_rs.base();

//计算偏移量

byte_map_base = _byte_map - (uintptr_t(low_bound) >> card_shift);

.....

}

网上许多文章会说卡表是在堆中的,然而从源码中我们可以看到严格来说并不是属于java_heap管理的,而是一段额外的数组进行管理。

我们再看看java_heap内存申请的代码:

//申请堆内存的方法,会在申请card_table之前申请

ReservedSpace Universe::reserve_heap(size_t heap_size, size_t alignment) {

......

//计算堆的地址

char* addr = Universe::preferred_heap_base(total_reserved, alignment, Universe::UnscaledNarrowOop);

//total_reserved是最大堆内存

//申请内存,这里会传入地址从特定地址开始申请,默认从0开始申请最大堆内存

ReservedHeapSpace total_rs(total_reserved, alignment, use_large_pages, addr);

.....

return total_rs;

}

//进入下面的初始化列表方法

ReservedHeapSpace::ReservedHeapSpace(size_t size, size_t alignment,

bool large, char* requested_address) :

//ReservedHeapSpace是ReservedSpace的子类底层还是会调用mmap()

ReservedSpace(size, alignment, large,

requested_address,

(UseCompressedOops && (Universe::narrow_oop_base() != NULL) &&

Universe::narrow_oop_use_implicit_null_checks()) ?

lcm(os::vm_page_size(), alignment) : 0) {

if (base() > 0) {

//注意这里标记的是mtJavaHeap,即为javaHeap申请的内存

MemTracker::record_virtual_memory_type((address)base(), mtJavaHeap);

}

protect_noaccess_prefix(size);

}

由于card_table在heap之后才会申请创建,且是随机映射,而heap是根据对应地址去映射,所以card_table并不是使用的heap空间。

三.记忆集合(Remembered Set)

了解了卡表和写屏障等相关知识,我们就可以继续看源码了,在应用中不免会存在跨代的引用关系,我们在youngGC时就不得不扫描老年代的region,甚至整个老年代,而老年代占堆的比例是相当大的,所以为了节省开销,增加效率就有了记忆集合(玩家时代:www.wanjiashidai.com),专门用来记录跨代引用,方便我们在GC的时候直接处理记忆集合从而避免遍历老年代,在每个region中都有一个记忆集合。

怎样才能完整的记录所有的跨代引用呢?再jvm中我们其实借助的是写屏障和卡表来记录,每次的引用修改都会执行我们的写屏障方法,而写屏障方法会把对应位置的卡页标记为脏卡,并加入脏卡队列中,这样所有的有效引用关关系都会在脏卡队列中,只要我们处理脏卡队列,就可以从中过滤出所有跨代引用。

脏卡队列一般是Refine线程异步处理,Refine线程中存在白,绿,黄,红四个标记,不同的标记处理脏卡队列的refine线程数不一样,当到达红标记时,Mutator线程(java应用线程)也参与处理(关于标记部分网上由许多文章讲的比较详细,笔者在这里就不过多阐述)。我们接着写屏障的源码继续看:

JRT_LEAF(void, SharedRuntime::g1_wb_post(void* card_addr, JavaThread* thread))

//获取java线程中的dcq将卡页入列

//enqueue入列方法最终会调用脏卡队列的父类PtrQueue的入列方法enqueue

thread->dirty_card_queue().enqueue(card_addr);

JRT_END

//脏卡队列类:DirtyCardQueue 继承 PtrQueue

//脏卡队列集合:DirtyCardQueueSet 继承 PtrQueueSet

//PtrQueue的入列方法

void enqueue(void* ptr) {

if (!_active) return;

//我们直接看这个方法

else enqueue_known_active(ptr);

}

//PtrQueue(DirtyCardQueue)内部有个_buf可以理解为时一个数组,默认容量是256

void PtrQueue::enqueue_known_active(void* ptr) {

//_index是下标,与一般下标不一样的是只有初始化和_buf满时_index会为0

while (_index == 0) {

//这个方法只有在初始化和扩容的时候会进入

handle_zero_index();

}

//每入列一个元素_index会减少

_index -= oopSize;

_buf[byte_index_to_index((int)_index)] = ptr;

}

//我们看下handle_zero_index()方法

void PtrQueue::handle_zero_index() {

//判断是初始化还是扩容为null则为初始化

//true为扩容

if (_buf != NULL) {

......

//判断是否有锁,这里只有shared dirty card queue会是true,因为shared_dirty_card_queue可能会有

//多个线程操作,关于shared dirty card queue笔者在讲youngGC的文章中有介绍,这里就不再阐述

if (_lock) {

void** buf = _buf; // local pointer to completed buffe

_buf = NULL; // clear shared _buf field

locking_enqueue_completed_buffer(buf); // enqueue completed buffe

if (_buf != NULL) return;

} else {

//我们来看这里,写屏障会调用这个方法

if (qset()->process_or_enqueue_complete_buffer(_buf)) {

_sz = qset()->buffer_size();

_index = _sz;

return;

}

}

}

//初始化queue申请_buf,修改_index

_buf = qset()->allocate_buffer();

_sz = qset()->buffer_size();

_index = _sz;

}

//这里会调用PtrQueueSet的方法

//每个java线程都有自己的DirtyCardQueue(PtrQueue)

//所有的DirtyCardQueue都关联一个全局DirtyCardQueueSet(PtrQueueSet)

bool PtrQueueSet::process_or_enqueue_complete_buffer(void** buf) {

//判断是否是java线程

if (Thread::current()->is_Java_thread()) {

//如果是java线程判断是否到达红标记(_max_completed_queue即red标记,在DirtyCardQueueSet初始化时会传入)

if (_max_completed_queue == 0 || _max_completed_queue > 0 &&

_n_completed_buffers >= _max_completed_queue + _completed_queue_padding) {

//达到红标记则自己处理

bool b = mut_process_buffer(buf);

if (b) {

return true;

}

}

}

//这个方法最后会将满的_buf加入DirtyCardQueueSet,自己再重新申请一个buf

enqueue_complete_buffer(buf);

return false;

}

这里我们稍微解释下DirtyCardQueue和DirtyCardQueueSet,每个java线程都有一个私有的DirtyCardQueue(PtrQueue),所有的DirtyCardQueue都关联一个全局DirtyCardQueueSet(PtrQueueSet),每个DirtyCardQueue默认大小为256,当一个DirtyCardQueue满了之后会将其中满的数组(_buf)添加到DirtyCardQueueSet中,并为DirtyCardQueue重新申请一个新的数组(_buf),关于这方面的知识笔者在之前将youngGC的文章也有过介绍,有兴趣的读者也可以看下。

其实Mutator线程(java应用线程)和Refine线程处理脏卡队列的最终方法都是一样的,只不过调用过程不一样,我们继续看下Mutator线程(java应用线程):

bool DirtyCardQueueSet::mut_process_buffer(void** buf) {

bool already_claimed = false;

//获取当前java线程

JavaThread* thread = JavaThread::current();

//获取线程的par_id

int worker_i = thread->get_claimed_par_id();

//如果worker_i不为-1就证明线程已经申请过par_id

if (worker_i != -1) {

already_claimed = true;

} else {

//否则重新获取个par_id

worker_i = _free_ids->claim_par_id();

//存储par_id

thread->set_claimed_par_id(worker_i);

}

bool b = false;

if (worker_i != -1) {

//这是处理脏卡队列的核心方法

//_closure参数是一个迭代器RefineCardTableEntryClosure

//buf是之前传入的脏卡队列中的数组

b = DirtyCardQueue::apply_closure_to_buffer(_closure, buf, 0,

_sz, true, worker_i);

if (b) Atomic::inc(&_processed_buffers_mut);

//如果是本次调用申请的par_id则要归还

if (!already_claimed) {

// 归还par_id

_free_ids->release_par_id(worker_i);

//同时将线程par_id设置为-1

thread->set_claimed_par_id(-1);

}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档