内存管理——记忆集和读写屏障

记忆集和读写屏障

由于对堆使用了分代管理,所以在对新生代进行GC的时候需要将老年代对新生代中对象的引用也当做根来看待,所以在根标记的过程中也需要对老年代的引用进行标记,为了快速遍历老年代,所以需要使用记忆集来记录老年代的哪些部分可能含有对新生代的引用,同时引用可能被修改所以就需要有读写屏障来对引用关系的修改进行追踪,以达到更新记忆集的目的。

这里的记忆集指的是CardTableRS,屏障指的是CardTableBarrierSet。由于假设读者知道记忆集和读写屏障是干什么的,所以直接从使用他们开始。

 

putfield

首先来看,在解释执行的时候如何修改一个域,从putfield开始看,直到看到和读写屏障有关的地方我们停下来去看读写屏障。来看一下关于putfield的字节码实现,其中删除了一些逻辑,只保留了修改的字段为int的情况:

 void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc) {
   transition(vtos, vtos);
 
   const Register cache = rcx; // 常量池缓存指针。
   const Register index = rdx; // 在常量池缓存中索引。
   const Register obj   = rcx; // 要进行修改的目标对象,可能是instance对象可能是class对象。
   const Register off   = rbx; // 偏移。
   const Register flags = rax;
   const Register bc    = LP64_ONLY(c_rarg3) NOT_LP64(rcx);
 
   resolve_cache_and_index(byte_no, cache, index, sizeof(u2)); // 尝试对字段进行解析,获取其在常量池缓存的索引。
   load_field_cp_cache_entry(obj, cache, index, off, flags, is_static); // 对缓存项进行加载,获取字段对应的偏移。
 
   // field addresses
   const Address field(obj, off, Address::times_1, 0*wordSize); // 构建地址。
 
   // itos
  {
     __ pop(itos); // 将int加载到rax。
     if (!is_static) pop_and_check_object(obj); // 获取obj,在load_field_cp_cache_entry并没有为putfield获取obj,只为putstatic获取了mirror。
     __ access_store_at(T_INT, IN_HEAP, field, rax, noreg, noreg); // 访问,待会重点看这里面生成了什么。
     if (!is_static && rc == may_rewrite) {
       patch_bytecode(Bytecodes::_fast_iputfield, bc, rbx, true, byte_no);
    }
     __ jmp(Done); // 结束。
  }
   __ bind(Done);
 }

关于常量池缓存和指令重写的内容在常量池缓存。那么进入MacroAssembler::access_store_at来看具体是如何进行的:

 void MacroAssembler::access_store_at(BasicType type, DecoratorSet decorators, Address dst, Register src,
                                      Register tmp1, Register tmp2) {
   BarrierSetAssembler* bs = BarrierSet::barrier_set()->barrier_set_assembler();
   decorators = AccessInternal::decorator_fixup(decorators);
   bool as_raw = (decorators & AS_RAW) != 0;
   if (as_raw) {
     bs->BarrierSetAssembler::store_at(this, decorators, type, dst, src, tmp1, tmp2);
  } else {
     bs->store_at(this, decorators, type, dst, src, tmp1, tmp2);
  }
 }

由于并不是AS_RAW的,所以只看第二个分支就行了,第一个分支中的bs->BarrierSetAssembler::store_at,平时不是很常见,BarrierSetAssembler::store_at是一个虚函数,我们平时都希望使用子类覆盖的实现,但是这里却强制使用了在BarrierSetAssembler定义的实现。

我们进入到store_at的具体实现来看看,唯一一个覆盖其实就是ModRefBarrierSetAssembler::store_at

 void ModRefBarrierSetAssembler::store_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type,
                                          Address dst, Register val, Register tmp1, Register tmp2) {
   if (type == T_OBJECT || type == T_ARRAY) {
     oop_store_at(masm, decorators, type, dst, val, tmp1, tmp2);
  } else {
     BarrierSetAssembler::store_at(masm, decorators, type, dst, val, tmp1, tmp2);
  }
 }

我们可以看到第二个分支还是使用了BarrierSetAssembler::store_at,所以我们来看看里面生成了哪些代码,里面其实并没有使用记忆集,所以在使用as_raw的时候会直接使用这个方法:

 void BarrierSetAssembler::store_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type,
                                    Address dst, Register val, Register tmp1, Register tmp2) {
   bool in_heap = (decorators & IN_HEAP) != 0;
   bool in_native = (decorators & IN_NATIVE) != 0;
   bool is_not_null = (decorators & IS_NOT_NULL) != 0;
   bool atomic = (decorators & MO_RELAXED) != 0;
 
   switch (type) {
   case T_OBJECT:
   case T_ARRAY: {
     if (in_heap) {
       if (val == noreg) { // 传入NULL。
         assert(!is_not_null, "inconsistent access");
         if (UseCompressedOops) {
           __ movl(dst, (int32_t)NULL_WORD);
        } else {
           __ movslq(dst, (int32_t)NULL_WORD);
        }
      } else {
         if (UseCompressedOops) {
           assert(!dst.uses(val), "not enough registers");
           if (is_not_null) {
             __ encode_heap_oop_not_null(val); // 存储时修改为32位表示。
          } else {
             __ encode_heap_oop(val);
          }
           __ movl(dst, val);
        } else {
           __ movptr(dst, val);
        }
      }
    } else {
       assert(in_native, "why else?");
       assert(val != noreg, "not supported");
       __ movptr(dst, val);
    }
     break;
  }
   case T_BOOLEAN:
     __ andl(val, 0x1);  // boolean is true if LSB is 1
     __ movb(dst, val);
     break;
   case T_BYTE:
     __ movb(dst, val);
     break;
   case T_SHORT:
     __ movw(dst, val);
     break;
   case T_CHAR:
     __ movw(dst, val);
     break;
   case T_INT:
     __ movl(dst, val);
     break;
   case T_LONG:
     assert(val == noreg, "only tos");
     __ movq(dst, rax);
     break;
   case T_FLOAT:
     assert(val == noreg, "only tos");
     __ store_float(dst);
     break;
   case T_DOUBLE:
     assert(val == noreg, "only tos");
     __ store_double(dst);
     break;
   case T_ADDRESS:
     __ movptr(dst, val);
     break;
   default: Unimplemented();
  }
 }

可以看到除了T_OBJECTT_ARRAY之外的实现都非常简单,就是将val直接放到dst,T_OBJECT也只是多了关于指针压缩和NULL的逻辑。

继续回到ModRefBarrierSetAssembler::store_at

   if (type == T_OBJECT || type == T_ARRAY) {
     oop_store_at(masm, decorators, type, dst, val, tmp1, tmp2);
  }

来看看这个方法会被覆盖,所以说实际上使用的是CardTableBarrierSetAssembler::oop_store_at,因为我们在创建堆的时候使用的就是CardTableBarrierSet。来看其实现:

 void CardTableBarrierSetAssembler::oop_store_at(MacroAssembler* masm, DecoratorSet decorators, BasicType type,
                                                 Address dst, Register val, Register tmp1, Register tmp2) {
   bool in_heap = (decorators & IN_HEAP) != 0; // 是否是java heap,这个heap是记忆集所管理的。
 
   bool is_array = (decorators & IS_ARRAY) != 0;
   bool on_anonymous = (decorators & ON_UNKNOWN_OOP_REF) != 0;
   bool precise = is_array || on_anonymous;
 
   bool needs_post_barrier = val != noreg && in_heap; // 如果noreg没必要使用记忆集,因为不可能会指向需要记录的地方。
 
   BarrierSetAssembler::store_at(masm, decorators, type, dst, val, noreg, noreg);
   if (needs_post_barrier) { // putfield会到这里。
     // flatten object address if needed
     if (!precise || (dst.index() == noreg && dst.disp() == 0)) {
       store_check(masm, dst.base(), dst); // 到这里
    } else {
       __ lea(tmp1, dst);
       store_check(masm, tmp1, dst);
    }
  }
 }

进一步进入CardTableBarrierSetAssembler::store_check

 void CardTableBarrierSetAssembler::store_check(MacroAssembler* masm, Register obj, Address dst) {
   // Does a store check for the oop in register obj. The content of
   // register obj is destroyed afterwards.
   BarrierSet* bs = BarrierSet::barrier_set();
 
   CardTableBarrierSet* ctbs = barrier_set_cast<CardTableBarrierSet>(bs);
   CardTable* ct = ctbs->card_table(); // 获取卡表。
   assert(sizeof(*ct->byte_map_base()) == sizeof(jbyte), "adjust this code");
 
   __ shrptr(obj, CardTable::card_shift); // 等价于除512,因为被管理的区域中每512B对应卡表中的一项,卡表中一项为1B。
 
   Address card_addr;
 
   // The calculation for byte_map_base is as follows:
   // byte_map_base = _byte_map - (uintptr_t(low_bound) >> card_shift);
   // So this essentially converts an address to a displacement and it will
   // never need to be relocated. On 64bit however the value may be too
   // large for a 32bit displacement.
   intptr_t byte_map_base = (intptr_t)ct->byte_map_base(); // 并不是卡表基地址,具体的内容看卡表的创建部分。
   if (__ is_simm32(byte_map_base)) { // 如果偏移量可以用32位有符号数表示。
     card_addr = Address(noreg, obj, Address::times_1, byte_map_base); // 获得对应项的地址,一个项对应512B。
  } else {
     // By doing it as an ExternalAddress 'byte_map_base' could be converted to a rip-relative
     // displacement and done in a single instruction given favorable mapping and a
     // smarter version of as_Address. However, 'ExternalAddress' generates a relocation
     // entry and that entry is not properly handled by the relocation code.
     AddressLiteral cardtable((address)byte_map_base, relocInfo::none);
     Address index(noreg, obj, Address::times_1);
     card_addr = __ as_Address(ArrayAddress(cardtable, index));
  }
 
   int dirty = CardTable::dirty_card_val();
   if (UseCondCardMark) { // UseCondCardMark默认为false,仅当不为dirty才设为脏。
     Label L_already_dirty;
     if (ct->scanned_concurrently()) {
       __ membar(Assembler::StoreLoad);
    }
     __ cmpb(card_addr, dirty);
     __ jcc(Assembler::equal, L_already_dirty);
     __ movb(card_addr, dirty);
     __ bind(L_already_dirty);
  } else {
     __ movb(card_addr, dirty); // 将对应区域设为脏。
  }
 }

从上面可以看出,并没有将一个区域重新设置为clean的逻辑,因为设置为clean需要进行同步,以及更多的运算,将一些工作留给GC会更加好。下面开始看看记忆集和读写屏障的创建。

 

创建和初始化

在对堆的创建和初始化中,记忆集和读写屏障也被创建和初始化了,这个内容在堆创建的那篇文章中提到过。这里再次给出有关代码:

 jint GenCollectedHeap::initialize() {
  // ...
 
   initialize_reserved_region((HeapWord*)heap_rs.base(), (HeapWord*)(heap_rs.base() + heap_rs.size()));
 
   _rem_set = create_rem_set(reserved_region());
   _rem_set->initialize();
   CardTableBarrierSet *bs = new CardTableBarrierSet(_rem_set);
   bs->initialize();
   BarrierSet::set_barrier_set(bs);
   
  // ...
 }

在保留区创建好之后创建了记忆集,然后使用记忆集对读写屏障进行了初始化和创建。

这几个函数并不复杂,所以我们可以一个一个地来看。

_rem_set = create_rem_set(reserved_region());

 CardTableRS* GenCollectedHeap::create_rem_set(const MemRegion& reserved_region) { 
   // 这个region从heap_rs.base()开始,长度为heap_rs.size()。
   return new CardTableRS(reserved_region, false /* scan_concurrently */);
 }

 

 void BarrierSet::set_barrier_set(BarrierSet* barrier_set) {
   assert(_barrier_set == NULL, "Already initialized");
   _barrier_set = barrier_set; // 在上面的时候我们也看到了通过BarrierSet::_barrier_set获取读写屏障。
 
   // The barrier set was not initialized when the this thread (the main thread)
   // was created, so the call to BarrierSet::on_thread_create() had to be deferred
   // until we have a barrier set. Now we have a barrier set, so we make the call.
   _barrier_set->on_thread_create(Thread::current()); // 对于CardTableBarrierSet什么都不会发生。
 }

 

BarrierSetAssembler

在第一节我们就看到了这个类,但是并没有进行任何深入,只看了其中几个方法,我们这里深入来看看这几个类。先看看部分的类继承体系,只和Serial收集器有关。

 
 
 
CardTableBarrierSetAssembler
ModRefBarrierSetAssembler
BarrierSetAssembler
 

从名字就可以看出来,这些汇编器是和读写屏障对应的。

 

 

patch

为什么会感觉和教科书中不同呢?记忆集不是应该要追踪old到young的引用么?为什么直接地进行了一个设值使其为dirty就完事了?其实是因为这样能够大大降低屏障的消耗,简单地将卡表置脏能够大大加快速度。

卡表本身会随着covered region的变化而变化,所以会自己管理自己的内存。除此之外,即使是young代也必须有对应的卡表,因为置脏的时候并不关系obj是否在old代中。



posted @ 2022-09-21 23:01  aana  阅读(121)  评论(0编辑  收藏  举报