筆者在以前講解g1 youngGC源碼的博客(https://my.oschina.net/u/3645114/blog/5119362)中提到過關於g1寫屏障和Rset(記憶集合)等相關知識點,以前限於文章長度(ps:所有介紹完博客會比較長)跳過了這個部分只是簡單介紹了下概念,今天咱們來繼續從源碼出發,探究g1的寫屏障和記憶集合等相關技術內幕。java
一.寫屏障(write barrier)
關於寫屏障,其實要從垃圾回收的三色標記提及,網上關於三色標記的文章不少,具體說明也比較詳細,筆者在這裏就不在進行詳細說明,本文的重點仍是放在源碼解析與閱讀上。node
在三色標記算法中,只有同時知足如下兩種條件就會產生漏標的問題:linux
- 灰色對象斷開了白色對象的引用(直接或間接的引用);即灰色對象原來成員變量的引用發生了變化。
- 黑色對象從新引用了該白色對象;即黑色對象成員變量增長了新的引用。
咱們只要破壞其中一個條件就能夠解決這個問題,而解決這個問題就須要用到讀屏障和寫屏障,在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源碼的路上幫助咱們披荊斬棘。