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

nanchenk發表於2018-07-02

前言

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演算法

V8 —— 你需要知道的垃圾回收機制
對於新生代物件,採用Scavenge演算法來回收。
簡單來說,將記憶體的空間分為兩個semispace,同一時刻只有一個空間處於使用中。使用中的叫做 to space,不被使用的叫做 from space。
分配物件時,先在From空間分配,垃圾回收時檢查(寬度優先)From空間的存活物件,將存活物件複製到To空間,清理非存活物件,複製後,空間身份發生對調。


Mark-Sweep演算法

V8 —— 你需要知道的垃圾回收機制
處理老生代物件時,採用深度優先掃描,用三色標記的演算法。
V8使用每個物件的兩個mark-bits和一個標記工作棧來實現標記。
兩個mark-bits編碼三種顏色:白色(00),灰色(10)和黑色(11)。
白色表示物件可以回收,黑色表示物件不能回收,並且他的所有引用都被便利完畢了,灰色表示不可回收,他的引用物件沒有掃描完畢。
掃描過程:
V8 —— 你需要知道的垃圾回收機制
  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

V8 —— 你需要知道的垃圾回收機制
平行標記期間,應用程式暫停,main thread和worker thread共同執行標記操作,下圖顯示了平行標記所涉及的資料結構。箭頭指示資料流的方向。
V8 —— 你需要知道的垃圾回收機制
其中,物件圖是隻讀的,不允許去修改他,Mark-bits和Marking worklist是可以讀和寫的。
Marking worklist負責決定分給其他worker thread的工作量,決定了效能與保持本地執行緒的均衡,所以如何高效地完成工作的分配至關重要。
如下圖所示,V8使用基於記憶體段的方式去平衡各個執行緒的工作量,避免執行緒同步的耗時與儘可能的工作。
V8 —— 你需要知道的垃圾回收機制


併發標記 concurrent marking

併發標記允許標記行為與應用程式同時進行。這就需要解決資料競爭的問題,比如JS程式碼在更改一個物件的欄位,而worker thread又在標記欄位,就可能導致錯誤的垃圾回收。
所以main thread需要與worker threads在發生資料競爭時進行同步,大多數的資料競爭行為通過輕量級的原子級記憶體訪問就可以同步,但是一些特殊的場景需要獨佔整個物件的訪問。


優化的結果

V8 —— 你需要知道的垃圾回收機制
有了平行標記與併發標記後,對比上面講的流程,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的幫助下,被平行標記


參考文獻:



相關文章