g1源碼從寫屏障到Rset全面解析

        筆者在以前講解g1 youngGC源碼的博客(https://my.oschina.net/u/3645114/blog/5119362)中提到過關於g1寫屏障和Rset(記憶集合)等相關知識點,以前限於文章長度(ps:所有介紹完博客會比較長)跳過了這個部分只是簡單介紹了下概念,今天咱們來繼續從源碼出發,探究g1的寫屏障和記憶集合等相關技術內幕。java

        一.寫屏障(write barrier)

關於寫屏障,其實要從垃圾回收的三色標記提及,網上關於三色標記的文章不少,具體說明也比較詳細,筆者在這裏就不在進行詳細說明,本文的重點仍是放在源碼解析與閱讀上。node

在三色標記算法中,只有同時知足如下兩種條件就會產生漏標的問題:linux

  1. 灰色對象斷開了白色對象的引用(直接或間接的引用);即灰色對象原來成員變量的引用發生了變化。
  2. 黑色對象從新引用了該白色對象;即黑色對象成員變量增長了新的引用。

咱們只要破壞其中一個條件就能夠解決這個問題,而解決這個問題就須要用到讀屏障和寫屏障,在jvm的垃圾回收器中,zgc使用的是讀屏障,筆者有篇相關博客專門介紹了zgc的技術內幕(http://www.javashuo.com/article/p-ecevhjei-vn.html),而咱們如今說的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) {
      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,甚至整個老年代,而老年代佔堆的比例是至關大的,因此爲了節省開銷,增長效率就有了記憶集合(Remembered Set),專門用來記錄跨代引用,方便咱們在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 buffer
      _buf = NULL;         // clear shared _buf field
      locking_enqueue_completed_buffer(buf);  // enqueue completed buffer
      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);
    }
  }
  return b;
}
bool DirtyCardQueue::apply_closure_to_buffer(CardTableEntryClosure* cl,
                                             void** buf,
                                             size_t index, size_t sz,
                                             bool consume,
                                             int worker_i) {
  if (cl == NULL) return true;
  //遍歷
  for (size_t i = index; i < sz; i += oopSize) {
    int ind = byte_index_to_index((int)i);
    //獲取card
    jbyte* card_ptr = (jbyte*)buf[ind];
    if (card_ptr != NULL) {
      if (consume) buf[ind] = NULL;
      //真正處理card的邏輯
      if (!cl->do_card_ptr(card_ptr, worker_i)) return false;
    }
  }
  return true;
}

到這裏咱們先停一下,一塊兒看下Refine線程是怎麼處理髒卡隊列的:

//因爲篇幅有限,筆者直接截取ConcurrentG1RefineThread類的run()方法
void ConcurrentG1RefineThread::run() {
      ......
    do {
      ......
      //調用髒卡隊列集合的方法
      //最後一個參數是綠標記
    } while (dcqs.apply_closure_to_completed_buffer(_worker_id + _worker_id_offset, cg1r()->green_zone()));
      ......
}

bool DirtyCardQueueSet::apply_closure_to_completed_buffer(int worker_i,
                                                          int stop_at,
                                                          bool during_pause) {
  //傳入的仍是_closure閉包即RefineCardTableEntryClosure
  return apply_closure_to_completed_buffer(_closure, worker_i,
                                           stop_at, during_pause);
}

bool DirtyCardQueueSet::apply_closure_to_completed_buffer(CardTableEntryClosure* cl,
                                                          int worker_i,
                                                          int stop_at,
                                                          bool during_pause) {
  //這個方法是獲取已經滿的buf
  //stop_at是以前傳入的綠標記,即Refine線程只處理綠標記以上的,而白標記到綠標記的部分只會在gc的時候處理
  BufferNode* nd = get_completed_buffer(stop_at);
  //進行處理
  bool res = apply_closure_to_completed_buffer_helper(cl, worker_i, nd);
  if (res) Atomic::inc(&_processed_buffers_rs_thread);
  return res;
}

bool DirtyCardQueueSet::
apply_closure_to_completed_buffer_helper(CardTableEntryClosure* cl,
                                         int worker_i,
                                         BufferNode* nd) {
  if (nd != NULL) {
    void **buf = BufferNode::make_buffer_from_node(nd);
    size_t index = nd->index();
    //能夠看到仍是調用和Mutator一樣的方法
    //apply_closure_to_buffer
    bool b =
      DirtyCardQueue::apply_closure_to_buffer(cl, buf,
                                              index, _sz,
                                              true, worker_i);
    if (b) {
      deallocate_buffer(buf);
      return true;  // In normal case, go on to next buffer.
    } else {
      enqueue_complete_buffer(buf, index);
      return false;
    }
  } else {
    return false;
  }
}

咱們看到Refine和Mutator最終的處理方式都是調用apply_closure_to_buffer方法以後調用閉包的do_card_ptr()

關於do_card_ptr()這個方法,筆者在講youngGC的文章中也提到過,只不過在youngGC中用的的閉包和如今提到的閉包不同,

在youngGC中是:RefineRecordRefsIntoCSCardTableEntryClosure

在Refine和Mutator線程中都是:RefineCardTableEntryClosure

咱們分別把這兩個閉包的do_card_ptr方法拿出來看看:

//RefineCardTableEntryClosure
bool do_card_ptr(jbyte* card_ptr, int worker_i) {
    bool oops_into_cset = _g1rs->refine_card(card_ptr, worker_i, false);
    if (_concurrent && _sts->should_yield()) {
      return false;
    }
    return true;
}
//RefineRecordRefsIntoCSCardTableEntryClosure
bool do_card_ptr(jbyte* card_ptr, int worker_i) {
    if (_g1rs->refine_card(card_ptr, worker_i, true)) {
      _into_cset_dcq->enqueue(card_ptr);
    }
    return true;
}

咱們看到最終都是調用這個方法_g1rs->refine_card()只不過第三個參數不一樣,關於這個參數咱們後面會講到。

_g1rs是G1RemSet類,是G1記憶集合的基類,到這裏就涉及到記憶集合了,咱們繼續看下這個方法refine_card():

bool G1RemSet::refine_card(jbyte* card_ptr, int worker_i,
                           bool check_for_refs_into_cset) {
  //判斷card是不是髒卡,若是不是則直接返回
  if (*card_ptr != CardTableModRefBS::dirty_card_val()) {
    return false;
  }
  //獲取card所映射的內存開始地址
  HeapWord* start = _ct_bs->addr_for(card_ptr);
  //找到card所在的region
  HeapRegion* r = _g1->heap_region_containing(start);
  //判斷是否爲空
  if (r == NULL) {
    return false; 
  }
  //判斷是否是young,這裏是判斷引用所在的region是不是young,由於在youngGc時,咱們只關注old->young的引用關係
  //這樣才能避免咱們遍歷全部老年代,若是是young->young或young->old則只須要判斷其是否在gcRoot的引用鏈
  if (r->is_young()) {
    return false;
  }
  //判斷在不在回收集合中,引用所在的region在回收集合中,就證實其在下次GC時會被掃描,因此也不用進入記憶集合
  if (r->in_collection_set()) {
    return false;
  }
  //熱卡緩存,判斷是否用了熱卡緩存,若是用了則加入
  G1HotCardCache* hot_card_cache = _cg1r->hot_card_cache();
  if (hot_card_cache->use_cache()) {
    card_ptr = hot_card_cache->insert(card_ptr);
    if (card_ptr == NULL) {
      // There was no eviction. Nothing to do.
      return false;
    }

    start = _ct_bs->addr_for(card_ptr);
    r = _g1->heap_region_containing(start);
    if (r == NULL) {
      return false;
    }
  }
  //計算card映射的內存終點
  HeapWord* end   = start + CardTableModRefBS::card_size_in_words;
  //聲明髒區域,即card映射的內存區
  MemRegion dirtyRegion(start, end);

#if CARD_REPEAT_HISTO
  init_ct_freq_table(_g1->max_capacity());
  ct_freq_note_card(_ct_bs->index_for(start));
#endif

  OopsInHeapRegionClosure* oops_in_heap_closure = NULL;
  //這個參數時以前咱們提到的第三個參數
  //youngGC在這裏是true會使用youngGC的閉包,會將引用push到一個隊列中(筆者在youngGC的文章中講過)
  //refine這裏則是false,oops_in_heap_closure爲Null
  if (check_for_refs_into_cset) {
    oops_in_heap_closure = _cset_rs_update_cl[worker_i];
  }
  //以後聲明瞭許多閉包,咱們重點關注下這個,是更新Rs或者入列引用的閉包
  G1UpdateRSOrPushRefOopClosure update_rs_oop_cl(_g1,
                                                 _g1->g1_rem_set(),
                                                 oops_in_heap_closure,
                                                 check_for_refs_into_cset,
                                                 worker_i);
  update_rs_oop_cl.set_from(r);

  G1TriggerClosure trigger_cl;
  FilterIntoCSClosure into_cs_cl(NULL, _g1, &trigger_cl);
  G1InvokeIfNotTriggeredClosure invoke_cl(&trigger_cl, &into_cs_cl);
  G1Mux2Closure mux(&invoke_cl, &update_rs_oop_cl);
  //這個閉包封裝了G1UpdateRSOrPushRefOopClosure,能夠看到當oops_in_heap_closure爲null
  //會直接使用update_rs_oop_cl
  FilterOutOfRegionClosure filter_then_update_rs_oop_cl(r,
                        (check_for_refs_into_cset ?
                                (OopClosure*)&mux :
                                (OopClosure*)&update_rs_oop_cl));

  bool filter_young = true;
  //咱們進入這個方法看下
  HeapWord* stop_point =
    r->oops_on_card_seq_iterate_careful(dirtyRegion,
                                        &filter_then_update_rs_oop_cl,
                                        filter_young,
                                        card_ptr);
  ......
}

HeapWord* HeapRegion::oops_on_card_seq_iterate_careful(MemRegion mr,
                                 FilterOutOfRegionClosure* cl,
                                 bool filter_young,
                                 jbyte* card_ptr) {
  //先判斷是否時年輕代(略)
  .....

  //這裏會把card髒標記去掉,進入rset的card都是乾淨的標記的髒卡
  if (card_ptr != NULL) {
    *card_ptr = CardTableModRefBS::clean_card_val();
    OrderAccess::storeload();
  }
  // 計算 Region的邊界
  HeapWord* const start = mr.start();
  HeapWord* const end = mr.end();
 //尋找跨越start的對象初始地址,HeapWord* 是一個指向堆的指針
  HeapWord* cur = block_start(start);
  oop obj;
  HeapWord* next = cur;
//處理跨越start的對象
  while (next <= start) {
    cur = next;
    obj = oop(cur);
//對象在region上是連續排列的,若是爲null則後面沒有對象了
    if (obj->klass_or_null() == NULL) {
      return cur;
    }
    next = (cur + obj->size());
  }
  //判斷對象是否存活
  if (!g1h->is_obj_dead(obj)) {
  //這裏會調用不少宏命令定義的方法,最後會用傳入的閉包G1UpdateRSOrPushRefOopClosure進行遍歷
    obj->oop_iterate(cl, mr);
  }
 //繼續查看後面的對象
  while (cur < end) {
    obj = oop(cur);
    if (obj->klass_or_null() == NULL) {
      // Ran into an unparseable point.
      return cur;
    };
    next = (cur + obj->size());
//判斷是否存活
    if (!g1h->is_obj_dead(obj)) {
      if (next < end || !obj->is_objArray()) {
  //對象不跨region,也不是數組
        obj->oop_iterate(cl);
      } else {
   //處理array
        obj->oop_iterate(cl, mr);
      }
    }
    cur = next;
  }
  return NULL;
}

看到這裏讀者可能會疑惑爲何計算出card映射的區域以後要遍歷?由於以前咱們講過每一個卡頁映射的區域都是512字節,當這個卡頁被標記爲髒時,說明這512字節中會存在被修改的引用,因此咱們要遍歷這個區域的全部引用。

接下來就能夠鎖定核心方法,咱們以前提到的閉包G1UpdateRSOrPushRefOopClosure的G1UpdateRSOrPushRefOopClosure::do_oop_nv()方法:

//閉包方法
inline void G1UpdateRSOrPushRefOopClosure::do_oop_nv(T* p) {
  //將P轉換爲oop
  oop obj = oopDesc::load_decode_heap_oop(p);
  //獲取被引用對象所在的region
  HeapRegion* to = _g1->heap_region_containing(obj);
  //_from是以前set進來的原引用持有對象所在card所在的region
  if (to != NULL && _from != to) {
    //這裏是以前refine_card方法傳入的第三個參數,存在三種狀況
    // 1.Refine線程異步更新,則傳入false,直接更新rset
    // 2.youngGC時進行updateRset時,傳入的是true,若是引用的對象屬於回收集合則push到引用遷移集合
    // 以後會進行對象copy和修改引用
    //  3.youngGC是引用對象不屬於回收集合則更新rset
    if (_record_refs_into_cset && to->in_collection_set()) {
      if (!self_forwarded(obj)) {
        _push_ref_cl->do_oop(p);
      }
      return;
    }
    //更新to的RSet
    to->rem_set()->add_reference(p, _worker_i);
  }
}

這裏咱們能夠看到在youngGC時被引用的對象若是在回收集合中則會直接Push到遷移集合等待後面遷移處理,在Refine和Mutator線程中則會更新Rem_set,即記憶集合。

這裏咱們只關注更新記憶集合的方法:

void OtherRegionsTable::add_reference(OopOrNarrowOopStar from, int tid) {
  ......
  //計算引用所在的card
  int from_card = (int)(uintptr_t(from) >> CardTableModRefBS::card_shift);

  ......

  //獲取card所在的region和region_id
  HeapRegion* from_hr = _g1h->heap_region_containing_raw(from);
  RegionIdx_t from_hrs_ind = (RegionIdx_t) from_hr->hrs_index();

  //若是在粗粒度位圖中,直接返回
  //這個粗粒度位圖的key是region_id
  if (_coarse_map.at(from_hrs_ind)) {
    return;
  }

  //找到細粒度PerRegionTable
  //region_id根據細粒度PerRegionTable的最大容量取模
  size_t ind = from_hrs_ind & _mod_max_fine_entries_mask;
  //獲取對應的細粒度PerRegionTable
  PerRegionTable* prt = find_region_table(ind, from_hr);
  //PerRegionTable不存在
  if (prt == NULL) {
    MutexLockerEx x(&_m, Mutex::_no_safepoint_check_flag);
    //再次確認是否已經存在對應ID的PerRegionTable
    prt = find_region_table(ind, from_hr);
    if (prt == NULL) {
      ......
      //獲取card_index
      CardIdx_t card_index = from_card - from_hr_bot_card_index;
      if (G1HRRSUseSparseTable &&
          //直接加入稀疏表,若是成功則返回,失敗則繼續執行
          _sparse_table.add_card(from_hrs_ind, card_index)) {
        ......

        return;
      } else {
        //打印稀疏表滿了的日誌
        if (G1TraceHeapRegionRememberedSet) {
          gclog_or_tty->print_cr("   [tid %d] sparse table entry "
                        "overflow(f: %d, t: %d)",
                        tid, from_hrs_ind, cur_hrs_ind);
        }
      }
      //判斷細粒度PerRegionTable是否滿了
      if (_n_fine_entries == _max_fine_entries) {
        //若是滿了則刪除當前表並加入粗粒度位圖中
        prt = delete_region_table();
        //再從新初始化
        prt->init(from_hr, false /* clear_links_to_all_list */);
      } else {
        //若是沒滿,則證實沒有這個細粒度PerRegionTable申請並與全部細粒度PerRegionTable關聯
        prt = PerRegionTable::alloc(from_hr);
        link_to_all(prt);
      }
      //將新申請的或者初始化的細粒度PerRegionTable加入細粒度PerRegionTable表集合中
      PerRegionTable* first_prt = _fine_grain_regions[ind];
      prt->set_collision_list_next(first_prt);
      _fine_grain_regions[ind] = prt;
      _n_fine_entries++;
      
      if (G1HRRSUseSparseTable) {
        //獲取對應的region_id稀疏表,遍歷並將其加入細粒度PerRegionTable表
        //對應稀疏表滿了因此須要刪除並退化爲細粒度表
        SparsePRTEntry *sprt_entry = _sparse_table.get_entry(from_hrs_ind);
        for (int i = 0; i < SparsePRTEntry::cards_num(); i++) {
          CardIdx_t c = sprt_entry->card(i);
          if (c != SparsePRTEntry::NullEntry) {
            prt->add_card(c);
          }
        }
        //刪除稀疏表
        bool res = _sparse_table.delete_entry(from_hrs_ind);
      }
    }
  }

  //將card加入PerRegionTable的位圖中,這個位圖key是card_id
  //這個方法就不進行展開
  prt->add_reference(from);

  ......
}

這裏涉及了記憶集合(Rset)的三級數據結構,咱們這裏簡單畫張圖講述下:

首先,根據card找到其對應的region_id和card_id,將其添加到RHashTable中,RHashTable能夠看做一個hash表,key是region_id,value是card_id的數組。即先根據region_id找到對應的桶位(SparsePRTEntry),而後將card_id加入card_id數組中。若一個SparsePRTEntry滿了,則會先擴容,若是達到最大容量就會退化爲細粒度PerRegionTable的鏈表,即建立一個PerRegionTable(以region_id爲維度),其內部是一個bitMap位圖,key是card_id,若是對應card_id的card有跨代引用則value爲1,反之則爲1。當PerRegionTable鏈表也達到最大容量時,就會繼續退化爲一個粗粒度BitMap位圖,key是region_id,value表示這個region是否有引用到Rset所在的region,並清空PerRegionTable。這時候雖然會丟失這個region中card到Rset所在Region的引用細節,但證實有這個region中有不少引用到Rset所在的region,因此再gc時遍歷region並不會損失不少cpu性能。

最後:

至此,從寫屏障到記憶集合的源碼已經所有結束了。關於寫屏障,卡表和記憶集合,三個簡單的概念就能挖掘出如此多的細節,也令筆者很是驚訝。這部分代碼功能雖然僅僅是爲了破壞三色標記時漏標的條件,解決跨代引用,提高GC效率的,但其實現的細節卻至關複雜繁瑣。不過仔細想一想,咱們平時工做中,許多看似簡單的功能,實現起來也須要一步一步的推衍和設計,迭代,最終才能達到目標需求,從過程上也是相互照應的。

筆者再看源碼以前對於這關於寫屏障,卡表和記憶集合的概念一直很模糊,從而沒法理解G1的GC源碼,只有真正看了關於這部分的源碼,分析源碼才能瞭解到jvm的設計原理和實現細節,而且在繼續學習G1源碼的路上幫助咱們披荊斬棘。

相關文章
相關標籤/搜索