三色標記法與讀寫屏障

  前言算法

  本文主要介紹了三色標記法的基本思路、多標緻使的浮動垃圾、漏標的處理方案(讀寫屏障)等。緩存

  1. 垃圾回收的簡單回顧安全

  關於垃圾回收算法,基本就是那麼幾種:標記-清除、標記-複製、標記-整理。在此基礎上能夠增長分代(新生代/老年代),每代採起不一樣的回收算法,以提升總體的分配和回收效率。併發

  不管使用哪一種算法,標記老是必要的一步。這是理算固然的,你不先找到垃圾,怎麼進行回收?oop

  垃圾回收器的工做流程大致以下:post

  標記出哪些對象是存活的,哪些是垃圾(可回收);性能

  進行回收(清除/複製/整理),若是有移動過對象(複製/整理),還須要更新引用。優化

  本文着重來看下標記的部分。spa

  2. 三色標記法.net

  2.1 基本算法

  要找出存活對象,根據可達性分析,從GC Roots開始進行遍歷訪問,可達的則爲存活對象:

  

 

  最終結果:A/D/E/F/G 可達

  咱們把遍歷對象圖過程當中遇到的對象,按「是否訪問過」這個條件標記成如下三種顏色:

  白色:還沒有訪問過。

  黑色:本對象已訪問過,並且本對象 引用到 的其餘對象 也所有訪問過了。

  灰色:本對象已訪問過,可是本對象 引用到 的其餘對象 還沒有所有訪問完。所有訪問後,會轉換爲黑色。

  

 

  三色標記遍歷過程

  假設如今有白、灰、黑三個集合(表示當前對象的顏色),其遍歷訪問過程爲:

  初始時,全部對象都在 【白色集合】中;

  將GC Roots 直接引用到的對象 挪到 【灰色集合】中;

  從灰色集合中獲取對象:

  3.1. 將本對象 引用到的 其餘對象 所有挪到 【灰色集合】中;

  3.2. 將本對象 挪到 【黑色集合】裏面。

  重複步驟3,直至【灰色集合】爲空時結束。

  結束後,仍在【白色集合】的對象即爲GC Roots 不可達,能夠進行回收。

  注:若是標記結束後對象仍爲白色,意味着已經「找不到」該對象在哪了,不可能會再被從新引用。

  當Stop The World (如下簡稱 STW)時,對象間的引用 是不會發生變化的,能夠輕鬆完成標記。

  而當須要支持併發標記時,即標記期間應用線程還在繼續跑,對象間的引用可能發生變化,多標和漏標的狀況就有可能發生。

  2.2 多標-浮動垃圾

  假設已經遍歷到E(變爲灰色了),此時應用執行了 objD.fieldE = null :

  

 

  D > E 的引用斷開

  此刻以後,對象E/F/G是「應該」被回收的。然而由於E已經變爲灰色了,其仍會被看成存活對象繼續遍歷下去。最終的結果是:這部分對象仍會被標記爲存活,即本輪GC不會回收這部份內存。

  這部分本應該回收 可是 沒有回收到的內存,被稱之爲「浮動垃圾」。浮動垃圾並不會影響應用程序的正確性,只是須要等到下一輪垃圾回收中才被清除。

  另外,針對併發標記開始後的新對象,一般的作法是直接所有當成黑色,本輪不會進行清除。這部分對象期間可能會變爲垃圾,這也算是浮動垃圾的一部分。

  2.3 漏標-讀寫屏障

  假設GC線程已經遍歷到E(變爲灰色了),此時應用線程先執行了:

  var G = objE.fieldG;

  objE.fieldG = null; // 灰色E 斷開引用 白色G

  objD.fieldG = G; // 黑色D 引用 白色G

  

 

  E > G 斷開,D引用 G

  此時切回GC線程繼續跑,由於E已經沒有對G的引用了,因此不會將G放到灰色集合;儘管由於D從新引用了G,但由於D已是黑色了,不會再從新作遍歷處理。

  最終致使的結果是:G會一直停留在白色集合中,最後被看成垃圾進行清除。這直接影響到了應用程序的正確性,是不可接受的。

  不難分析,漏標只有同時知足如下兩個條件時纔會發生:

  條件一:灰色對象 斷開了 白色對象的引用;即灰色對象 原來成員變量的引用 發生了變化。

  條件二:黑色對象 從新引用了 該白色對象;即黑色對象 成員變量增長了 新的引用。

  從代碼的角度看:

  var G = objE.fieldG; // 1.讀

  objE.fieldG = null; // 2.寫

  objD.fieldG = G; // 3.寫

  讀取 對象E的成員變量fieldG的引用值,即對象G;

  對象E 往其成員變量fieldG,寫入 null值。

  對象D 往其成員變量fieldG,寫入 對象G ;

  咱們只要在上面這三步中的任意一步中作一些「手腳」,將對象G記錄起來,而後做爲灰色對象再進行遍歷便可。好比放到一個特定的集合,等初始的GC Roots遍歷完(併發標記),該集合的對象 遍歷便可(從新標記)。

  從新標記是須要STW的,由於應用程序一直在跑的話,該集合可能會一直增長新的對象,致使永遠都跑不完。固然,併發標記期間也能夠將該集合中的大部分先跑了,從而縮短從新標記STW的時間,這個是優化問題了。

  寫屏障用於攔截第二和第三步;而讀屏障則是攔截第一步。

  它們的攔截的目的很簡單:就是在讀寫先後,將對象G給記錄下來。

  2.3.1 寫屏障(Store Barrier)

  給某個對象的成員變量賦值時,其底層代碼大概長這樣:

  /**

  * @param field 某對象的成員變量,如 D.fieldG

  * @param new_value 新值,如 null

  */

  void oop_field_store(oop* field, oop new_value) {

  *field = new_value; // 賦值操做

  }

  所謂的寫屏障,其實就是指在賦值操做先後,加入一些處理(能夠參考AOP的概念):

  void oop_field_store(oop* field, oop new_value) {

  pre_write_barrier(field); // 寫屏障-寫前操做

  *field = new_value;

  post_write_barrier(field, value); // 寫屏障-寫後操做

  }

  (1) 寫屏障 + SATB

  當對象E的成員變量的引用發生變化時(objE.fieldG = null;),咱們能夠利用寫屏障,將E原來成員變量的引用對象G記錄下來:

  void pre_write_barrier(oop* field) {

  oop old_value = *field; // 獲取舊值

  remark_set.add(old_value); // 記錄 原來的引用對象

  }

  【當原來成員變量的引用發生變化以前,記錄下原來的引用對象】

  這種作法的思路是:嘗試保留開始時的對象圖,即原始快照(Snapshot At The Beginning,SATB),當某個時刻 的GC Roots肯定後,當時的對象圖就已經肯定了。

  好比 當時 D是引用着G的,那後續的標記也應該是按照這個時刻的對象圖走(D引用着G)。若是期間發生變化,則能夠記錄起來,保證標記依然按照本來的視圖來。

  值得一提的是,掃描全部GC Roots 這個操做(即初始標記)一般是須要STW的,不然有可能永遠都掃不完,由於併發期間可能增長新的GC Roots。

  SATB破壞了條件一:【灰色對象 斷開了 白色對象的引用】,從而保證了不會漏標。

  一點小優化:若是不是處於垃圾回收的併發標記階段,或者已經被標記過了,實際上是不必再記錄了,因此能夠加個簡單的判斷:

  void pre_write_barrier(oop* field) {

  // 處於GC併發標記階段 且 該對象沒有被標記(訪問)過

  if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {

  oop old_value = *field; // 獲取舊值

  remark_set.add(old_value); // 記錄 原來的引用對象

  }

  }

  (2) 寫屏障 + 增量更新

  當對象D的成員變量的引用發生變化時(objD.fieldG = G;),咱們能夠利用寫屏障,將D新的成員變量引用對象G記錄下來:

  void post_write_barrier(oop* field, oop new_value) {

  if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {

  remark_set.add(new_value); // 記錄新引用的對象

  }

  }

  【當有新引用插入進來時,記錄下新的引用對象】

  這種作法的思路是:不要求保留原始快照,而是針對新增的引用,將其記錄下來等待遍歷,即增量更新(Incremental Update)。

  增量更新破壞了條件二:【黑色對象 從新引用了 該白色對象】,從而保證了不會漏標。

  2.3.2 讀屏障(Load Barrier)

  oop oop_field_load(oop* field) {

  pre_load_barrier(field); // 讀屏障-讀取前操做  鄭州專業的男科醫院http://www.zzchanghong110.com/

  return *field;

  }

  讀屏障是直接針對第一步:var G = objE.fieldG;,當讀取成員變量時,一概記錄下來:

  void pre_load_barrier(oop* field, oop old_value) {

  if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) {

  oop old_value = *field;

  remark_set.add(old_value); // 記錄讀取到的對象

  } 鄭州看男科醫院哪家好http://www.120zzxbyy.com/

  }

  這種作法是保守的,但也是安全的。由於條件二中【黑色對象 從新引用了 該白色對象】,從新引用的前提是:得獲取到該白色對象,此時已經讀屏障就發揮做用了。

  2.4 三色標記法與現代垃圾回收器

  現代追蹤式(可達性分析)的垃圾回收器幾乎都借鑑了三色標記的算法思想,儘管實現的方式不盡相同:好比白色/黑色集合通常都不會出現(可是有其餘體現顏色的地方)、灰色集合能夠經過棧/隊列/緩存日誌等方式進行實現、遍歷方式能夠是廣度/深度遍歷等等。

  對於讀寫屏障,以Java HotSpot VM爲例,其併發標記時對漏標的處理方案以下:

  CMS:寫屏障 + 增量更新

  G1:寫屏障 + SATB

  ZGC:讀屏障

  工程實現中,讀寫屏障還有其餘功能,好比寫屏障能夠用於記錄跨代/區引用的變化,讀屏障能夠用於支持移動對象的併發執行等。功能以外,還有性能的考慮,因此對於選擇哪一種,每款垃圾回收器都有本身的想法。

  值得注意的是,CMS中使用的增量更新,在從新標記階段,除了須要遍歷 寫屏障的記錄,還須要從新掃描遍歷GC Roots(固然標記過的無需再遍歷了),這是因爲CMS對於astore_x等指令不添加寫屏障的緣由,具體可參考這裏。

相關文章
相關標籤/搜索