V8引擎詳解(七)——垃圾回收機制

前言

本文是V8引擎詳解系列的第七篇,重點內容是關於V8的垃圾回收機制,以及V8對垃圾回收的優化策略,本文首先須要對內存結構有一個初步瞭解,不瞭解的能夠先看一下V8引擎詳解(六)——內存結構。 文末會有已經完成的系列文章的連接,本系列文章還在不斷更新歡迎持續關注。javascript

垃圾回收

咱們先簡單瞭解一下垃圾回收的概念,好比V8引擎在執行代碼的過程當中遇到了一個函數,那麼咱們會建立一個函數執行上下文環境並添加到 調用棧 頂部,函數的做用域裏面包含了函數中全部的變量信息,在執行過程當中咱們分配內存建立這些變量,當函數執行完畢後函數做用域會被銷燬,那麼這個做用域包含的變量也就失去了做用,那麼銷燬它們回收內存的過程,咱們就叫作垃圾回收。java

垃圾回收的過程是v8引擎自動幫咱們執行的,在絕大部分狀況下v8都能很好的完成這個過程,可是做爲一段程序,能幫咱們cover住的狀況是有限的,因此一旦咱們代碼不夠嚴謹,就會引起內存泄露。算法

你們都知道javascript語言的一個特色就是單線程,單線程意味着執行的代碼都是按順序執行的且同一時間也只能處理一個任務,那麼V8在執行垃圾回收任務的時候,其餘的任務都將處於等待狀態,直到垃圾回收任務結束後才能執行其餘任務,若是垃圾回收任務的執行時間過長就不可避免的對用戶體驗形成影響,V8爲了減小這種影響也作了一系列的優化,咱們一塊兒來看一下V8究竟是如何作垃圾回收的而且是如何優化的。編程

垃圾回收器

代際假說(The Generational Hypothesis)是垃圾回收領域中的一個重要術語, V8的垃圾回收的策略也是創建在該假說的基礎之上。
代際假說也很簡單,主要有兩個特色:瀏覽器

  • 大部分對象在內存中存在的時間很短,簡單來講,就是不少對象一經分配內存,很快就變得不可訪問。
  • 不死的對象,會活的更久。

基於這個這個假說 V8 纔會把堆分爲新生代和老生代兩個區域,同時設計了兩個垃圾回收器:緩存

  • 副垃圾回收器 負責新生代區域的垃圾回收
  • 主垃圾回收器 負責老生代區域的垃圾回收

(新生代和老生代已經在做者以前的文章介紹過,不瞭解的能夠看以前的文章)bash

副垃圾回收器(Scavenging)

副垃圾回收器主要用來回收新生代的垃圾,一般咱們新建立的對象都會先分配到新生代內存區中。
新生代內存區會分紅兩個部分(space),from spaceto space , 這兩個區域本質都是同樣的,都擁有兩個狀態 工做狀態空閒狀態且當一個爲工做狀態的時候另外一個必定是空閒狀態。併發

好比咱們新建立一個對象:編程語言

  • 會向內存堆中的新生代去分配,假如此時新生代中的from spcae 是工做狀態,那麼對象會分配到from space 中。
  • 通過一段時間程序運行,from space的的內存即將達到存儲的上限。
  • V8引擎此時執行一次垃圾清理操做,會將from space中再也不使用的對象(根節點沒法遍歷到的對象)進行標記。
  • 會將未被標記的對象進行復制,複製到空閒狀態的to space中而且有序的從新排列起來,再將from space進行清空操做,同時將from space 標記爲空閒狀態將to space標記爲工做狀態。

以上就是所謂的置換也能夠說是翻轉過程,由於這種複製操做須要時間成本,因此新生代的空間每每並不大,因此執行的也較爲頻繁。函數

隨着程序的運行,某些對象一直在被使用會持續的積壓在新生代區域,爲了解決這個問題,V8採用了 晉升機制 將知足條件的對象放到老生代內存區中存儲,釋放新生代內存區域的空間。

晉升機制的條件:

  • 經歷過一次Scavenging算法,且並未被標記清除的,也就是過一次翻轉置換操做的對象。
  • 在進行翻轉置換時,被複制的對象大於to space空間的25%。(from spaceto space 必定是同樣大的)

晉升後的對象分配到老生代內存區,便由老生代內存區來管理。

主垃圾回收器(Mark-Sweep & Mark-Compact)

主垃圾回收器主要用來回收老生代的垃圾,一般會有在新生代晉升後的對象以及初始佔用空間就很大的對象會存儲在老生代內存區。

主垃圾回收器採用的方法和次垃圾回收器的方法徹底不一樣,主垃圾回收器會先使用標記 - 清除(Mark-Sweep)的算法進行垃圾回收。

引用一下李兵老師的描述:

首先是標記過程階段。標記階段就是從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程當中,能到達的元素稱爲活動對象,沒有到達的元素就能夠判斷爲垃圾數據。

接下來就是垃圾的清除過程。它和副垃圾回收器的垃圾清除過程徹底不一樣,主垃圾回收器會直接將標記爲垃圾的數據清理掉。

(圖片來源: time.geekbang.org/column/arti…)
整個標記 - 清除(Mark-Sweep)的過程至關於清理上圖中紅色部分區域的過程。

可是咱們經過這種標記清除的方式進行內存清理會產生大量不連續的內存碎片,當咱們想要存儲一個大的對象的時候就可能沒有足夠的空間,那麼除了執行 標記 - 清除(Mark-Sweep) 算法外,還經過 標記 - 整理(Mark-Compact) 算法進行垃圾回收。

標記 - 整理(Mark-Compact) 算法主要也是分兩步:

  • 首先一樣是標記過程。
  • 將未標記的對象(存活對象)進行左移,移動完成後清理邊界外的內存。

V8經過標記 - 清除(Mark-Sweep) 以及 標記 - 整理(Mark-Compact) 兩種算法對老生代內存區進行垃圾回收,這就是主垃圾回收器的主要工做。

垃圾回收優化策略(Orinoco)

上文中描述的V8的兩個垃圾回收器所採用的方法其實在具備垃圾回收機制的編程語言中都是很是常見的。
評價一個垃圾回收機制好壞的一個重要標準是取決於執行垃圾回收時主線程掛起的時間,而V8爲了優化這一部分體驗(減小主線程掛起的時間),啓動代號爲Orinoco的垃圾回收器項目來專門進行垃圾回收策略的優化。

Orinoco共實現了三個優化

  • 並行垃圾回收 (parallel)
  • 增量垃圾回收 (incremental)
  • 併發垃圾回收 (concurrent)

並行垃圾回收

先說第一個優化 並行垃圾回收,咱們以前提到過新生代內存區老生代內存區根據以前講過的垃圾回收機制,咱們能夠肯定在新生代內存區中的對象和老生代內存區中的對象是徹底不一樣的,那麼也就是說新生代在執行 標記->複製->清理 的操做和老生代執行 標記->清理->緊湊 的操做是沒有任何依賴關係的。
因而Orinoco判斷將沒有依賴關係的垃圾清理邏輯(不止上述一種)經過並行執行的方式來優化減小執行垃圾回收佔用主進程的時間。因此Orinoco只須要開啓輔助幾個輔助進程就能夠同時完成垃圾清理的工做以下圖:

(圖片來源:v8.dev/blog/trash-…)

增量垃圾回收

第二個優化 增量垃圾回收, 雖然並行垃圾回收的並行機制能夠有效的減小主進程的佔用,可是面對一個大的對象一次執行標記也要話很長的時間,從2011年開始V8引入了增量標記機制,也就是增量垃圾回收機制

(圖片來源: v8.dev/blog/trash-…)

將一次大的任務分解爲更小的塊,容許應用程序在塊之間運行。
這種優化對於標記的實現帶來了很大的挑戰,如何保存當時的掃描結果?標記好的數據若是被主線程修改了,如何正確的處理?

因而V8採用了 標記位標記工做表 來實現標記。
標記位用來標記三種顏色:白色(00)灰色(10)黑色(11)

  • 最初狀態全部的對象都是 白色 也就是未被根節點引用到的對象。
  • 當垃圾回收程序發現一個對象被引用會將這個對象標記爲 灰色 並將其推入到 標記工做表 中。
  • 標記工做表 會訪問全部存在自身的 灰色 對象,並訪問該對象的全部子對象,結束後會將該對象標記爲黑色
  • 標記工做表 會持續的被注入灰色的對象(每發現一個新的要標記的對象都會注入到標記工做表中
  • 若是 標記工做表 中 沒有了灰色 的對象,那麼表明全部的對象都是 黑色 或者 白色,以後能夠放心的清理掉 白色 的對象。

整個過程如圖:

從根節點開始標記

遍歷處理

完成後的最終形態

這個過程是否是有點繞,那我舉個例子(不知道恰不恰當哈)

好比有一個小偷團伙

  • 警察抓到了小偷團伙的A(標記爲灰色),可是警察沒有辦法給他定罪只能交給法庭(標記工做表)。
  • 在法庭上 A 供出了團伙的成員 B ,警察將犯罪團伙的 B 抓了回來(標記爲灰色)交給了法庭(標記工做表)。
  • B 說團伙還有個 C,可是 C 是冤枉的沒有犯罪(默認標記白色)。
  • 至此結案,會先將 B 定罪(標記爲黑色)而後將 A 定罪(標記爲黑色),而後A B判刑。

那回到以前的問題,標記好的數據若是被主線程修改了,如何正確的處理? V8 使用了寫屏障(write-barrier) 機制來實現,這個機制也不難理解,簡單來講就是強制讓黑色的對象不能直接指向白色的對象。 好比咱們執行一個寫入操做:

// 調用 `object.field = value` 以後
write_barrier(object, field_offset, value) {
  if (color(object) == black && color(value) == white) {
    set_color(value, grey);
    marking_worklist.push(value);
  }
}
複製代碼

將新寫入的對象從初始的白色直接變爲灰色,那麼 標記工做表 就沒有空,那麼就繼續執行標記的過程,保證了正確的標記數據。

併發垃圾回收

併發垃圾回收和並行垃圾回收是徹底不一樣的用一張圖來表示

並行垃圾回收發生在主線程和工做線程上。應用程序在整個並行標記階段暫停。
併發垃圾回收主要發生在工做線程上。當併發垃圾回收正在進行時,應用程序能夠繼續運行。

一般以上三種方式也不是單獨存在的,而是聚合在一塊兒使用具體以下圖:

空閒時垃圾回收

空閒時垃圾回收並不屬於Orinoco項目,是V8實現的一種優化策略。
一般調度程序經過對任務隊列佔用率的瞭解,以及和V8其餘組件接收到的信號,使它能夠估計V8什麼時候處於空閒狀態,以及可能保持多長時間。利用這個信息,V8能夠分配一些優先級不高的垃圾回收任務在這個空閒時間去作。

好比V8會使用Chrome瀏覽器的task scheduler , 根據從Chrome其餘各類組件接收到的信號以及旨在估算用戶意圖的各類啓發式方法,動態地從新分配任務的優先級。例如,若是用戶觸摸屏幕,則調度程序將在100毫秒的時間段內優先處理屏幕渲染和輸入任務,以確保用戶界面在用戶與網頁交互時保持響應。

例如,若是以60 FPS進行渲染,則幀間間隔爲16.6 ms。若是沒有在屏幕上進行任何有效的更新,則task scheduler 將啓動更長的空閒時間,該空閒時間持續到啓動下一個待處理任務爲止,且上限爲50毫秒,以確保Chrome保持對意外用戶輸入的響應。

更細節的關於空閒任務的描述能夠看 queue.acm.org/detail.cfm?… 這篇文章,本文很少贅述了。

總結

本文主要了解了V8的垃圾回收機制以及採用的一些優化方法,垃圾回收機制相對比較簡單,可是Orinoco優化的方法相對比較難以理解(做者尚未徹底理解併發垃圾回收究竟是如何作的因此沒有深刻的寫,後面理解清楚會更新),若是有什麼錯誤,請在評論中和做者一塊兒討論,若是您以爲本文對您有幫助請幫忙點個贊,感激涕零。

參考文章

queue.acm.org/detail.cfm?…
time.geekbang.org/column/arti…
v8.dev/blog/concur…
v8.js.cn/blog/orinoc…

系列文章

V8引擎詳解(一)——概述
V8引擎詳解(二)——AST
V8引擎詳解(三)——從字節碼看V8的演變
V8引擎詳解(四)——字節碼是如何執行的
V8引擎詳解(五)——內聯緩存
V8引擎詳解(六)——內存結構
V8引擎詳解(七)——垃圾回收機制

相關文章
相關標籤/搜索