V8 —— 你須要知道的垃圾回收機制

前言

V8 blog近日發佈了文章描述了「併發標記」的新技術,提高標記過程的效率。
併發標記是一個主要用新的平行和併發的垃圾收集器替換舊的垃圾回收器的項目,如今Chrome 64和Node.js v10已經默認啓用併發標記。講解以前咱們先回顧一下基本知識點。


基本概念

弱分代假設(The Weak Generational Hypothesis)

  1. 多數對象的生命週期短
  2. 生命週期長的對象,通常是常駐對象
V8的GC也是基於假設將對象分爲兩代: 新生代和老生代。
對不一樣的分代執行不一樣的算法能夠更有效的執行垃圾回收。


新生代與老生代

新生代包括一個New Space,老生代包括: Old Space, Code Space和Map Space,Large Object Space。
64位環境下的V8引擎的新生代內存大小32MB、老生代內存大小爲1400MB,而32位則減半,分別爲16MB和700MB。
對於新生代的對象,採用空間換取時間的Scavenge算法, 儘量快的回收內存。若是對象經歷了2次GC還依然堅挺,就會在第二次回收時晉升爲老生代(準確的說是保存在Old Space中)。
而老生代的GC採起Mark-Sweep的算法,並使用Mark-Sweep解決內存碎片的問題。


Scavenge算法

對於新生代對象,採用Scavenge算法來回收。
簡單來講,將內存的空間分爲兩個semispace,同一時刻只有一個空間處於使用中。使用中的叫作 to space,不被使用的叫作 from space。
分配對象時,先在From空間分配,垃圾回收時檢查(寬度優先)From空間的存活對象,將存活對象複製到To空間,清理非存活對象,複製後,空間身份發生對調。


Mark-Sweep算法

處理老生代對象時,採用深度優先掃描,用三色標記的算法。
V8使用每一個對象的兩個mark-bits和一個標記工做棧來實現標記。
兩個mark-bits編碼三種顏色:白色(00),灰色(10)和黑色(11)。
白色表示對象能夠回收,黑色表示對象不能回收,而且他的全部引用都被便利完畢了,灰色表示不可回收,他的引用對象沒有掃描完畢。
掃描過程:
  1. 從已知對象開始,即roots(全局對象和激活函數), 將全部非root對象標記置爲白色
  2. 將root對象的全部直接引用對象入棧(marking worklist)
  3. 依次pop出對象,出棧的對象標記爲黑,同時將他的直接引用對象標記爲灰色並push入棧
  4. 棧空的時候,仍然爲白色的對象能夠回收
  5. 回收白色的對象
在清除階段,只清除沒被標記的對象。
可是進行清除後,內存會出現不連續的狀態,對後續的大對象分配地址形成無心義的回收(由於可用內存的不足),這時就須要Mark-Compact來處理內存碎片了。


Mark-Compact算法

在對象標記死亡後,在整理的過程當中,將活着的對象向另外一個內存頁移動,移動完後內存頁就能夠還給操做系統,但若是這一頁的活動對象被不少其餘頁的對象引用,就不會compact,由於移動完後更新其餘引用的指針開銷大。


全暫停與增量標記

垃圾回收的3種基本算法須要應用邏輯暫停下來,垃圾回收完後恢復應用程序邏輯,即「全暫停」,過長的停頓會讓用戶感到卡頓,因此爲了下降全堆的垃圾回收,當堆的大小到必定程度後,開始增量GC,V8在標記階段將標記的動做分爲不少小「步進」,應用邏輯與垃圾回收交替進行直到標記階段完成。
可是,對於過大的堆,GC在試圖跟上應用程序分配速度的過程當中,仍有長時間的停頓,而且應用程序須要通知GC對象圖的全部變化,這些都是須要成本的(寫保障 write-barrier)。
V8使用Dijkstra-style 的寫屏障(write-barrier)來實現通知。
當object.field = value in JavaScript時,V8會插入如下代碼:
// Called after `object.field = value`.
write_barrier(object, field_offset, value) {
  if (color(object) == black && color(value) == white) {
    set_color(value, grey);
    marking_worklist.push(value);
  }
}
複製代碼
write-barrier能夠保障不會出現黑色對象指向了白色對象的現象發生(強三色不變形 strong tri-color invariant),這樣應用程序不會在GC時誤刪活動對象。在GC完成後全部白色對象都是可安全刪除的。
可是,因爲write-barrier的損耗,下降了應用程序的吞吐量,因此需用其餘的worker threads提升吞吐量,使worker threads也能夠進行標記的工做。這就是下面要講的平行標記和併發標記。


平行標記 parallel marking

平行標記期間,應用程序暫停,main thread和worker thread共同執行標記操做,下圖顯示了平行標記所涉及的數據結構。箭頭指示數據流的方向。
其中,對象圖是隻讀的,不容許去修改他,Mark-bits和Marking worklist是能夠讀和寫的。
Marking worklist負責決定分給其餘worker thread的工做量,決定了性能與保持本地線程的均衡,因此如何高效地完成工做的分配相當重要。
以下圖所示,V8使用基於內存段的方式去平衡各個線程的工做量,避免線程同步的耗時與儘量的工做。


併發標記 concurrent marking

併發標記容許標記行爲與應用程序同時進行。這就須要解決數據競爭的問題,好比JS代碼在更改一個對象的字段,而worker thread又在標記字段,就可能致使錯誤的垃圾回收。
因此main thread須要與worker threads在發生數據競爭時進行同步,大多數的數據競爭行爲經過輕量級的原子級內存訪問就能夠同步,可是一些特殊的場景須要獨佔整個對象的訪問。


優化的結果

有了平行標記與併發標記後,對比上面講的流程,GC的流程變爲:
  1. 從root對象開始掃描,填充對象到marking worklist
  2. 分佈併發標記任務到worker threads
  3. worker threads幫助main thread去更快地消費marking worklist中的對象
  4. main thread 偶爾會經過執行bailout worklist 和 marking worklist來marking
  5. 一旦marking worklists爲空,main thread 就完成GC行爲
  6. 在結束以前,main thread從新掃描roots,可能會發現其餘的白色節點,這些白色節點會在worker threads的幫助下,被平行標記


參考文獻:

相關文章
相關標籤/搜索