過去這些年 V8 的垃圾回收器(GC)發生了不少的變化,Orinoco 項目採用了 stop-the-world 垃圾回收器,以使其變成了一個更加並行,併發和增量的垃圾回收器。算法
咱們先來講說調用棧中數據是如何回收的垃圾的。首先是調用棧中的數據,咱們仍是經過一段示例代碼的執行流程來分析其回收機制,具體以下:bash
function foo(){
var a = 1
var b = {name:" 極客邦 "}
function showName(){
var c = " 極客時間 "
var d = {name:" 極客時間 "}
}
showName()
}
foo()
複製代碼
當執行到第 6 行代碼時,其調用棧和堆空間狀態圖以下所示: 併發
接着,當 showName 函數執行完成以後,函數執行流程就進入了 foo 函數,那這時就須要銷燬 showName 函數的執行上下文了。ESP(錄當前執行狀態的指針) 這時候就幫上忙了,JavaScript 會將 ESP 下移到 foo函數的執行上下文,這個下移操做就是銷燬 showName 函數執行上下文的過程。具體以下圖 函數
因此說,當一個函數執行結束以後,JavaScript 引擎會經過向下移動 ESP 來銷燬該函數保存在棧中的執行上下文。佈局
從上面咱們知道,當執行完foo函數以後,ESP 應該是指向全局執行上下文的,可是保存在堆中的兩個對象依然佔用着空間,以下圖所示: 動畫
要回收堆中的垃圾數據,就須要用到 JavaScript 中的垃圾回收器了。spa
在垃圾回收中有一個重要的術語:「代際假說」;代際假說代表不少對象在內存中存在的時間很短。換句話說,從垃圾回收的角度來看,不少對象一經分配內存空間隨即就變成了不可訪問的。這個假說不只僅適用於 V8 和 JavaScript, 代際假說有如下兩個特色: 1.第一個是大部分對象在內存中存在的時間很短,簡單來講,就是不少對象一經分配內存,很快就變得不可訪問; 2.第二個是不死的對象,會活得更久。線程
堆在 V8 中會分爲兩塊不一樣的區域,這兩塊區域分別稱之爲老生代和新生代,新生代又進一步分爲 ‘nursery’(from-space) 子代和 ‘intermediate’ (to-space)子代兩塊區域; 一個對象第一次分配內存時會被分配到新生代中的‘from-space’ 子代;若是進過下一次垃圾回收這個對象還存在新生代中,這時候咱們移動到 ‘to-space’ 子代,再通過下一次垃圾回收這個對象還在新生代,這時候咱們就會把這個對象移動到老生代。 3d
V8 有兩個垃圾回收器,一個是主垃圾回收器,一個是副垃圾回收器。主垃圾回收器從整個堆中回收垃圾,副垃圾回收器(Scavenger)重新生代中回收垃圾。主垃圾回收器能夠頗有效的從整個堆中回收垃圾,可是代際假說告訴咱們新分配內存的對象也極有可能須要垃圾回收。指針
副垃圾回收器只重新生代中回收垃圾,倖存的對象老是會被分配到內存頁中去。在清理時,初始的空閒區域稱之爲「To-Space」,複製對象過來的區域稱之爲「From-Space」;在最壞的狀況下,若是每個對象在清理的時候存活了下來,那咱們就要複製每個對象。
對於清理,咱們會維護一個額外的根集,這個根集裏會存放一些從舊到新的引用。這些引用是在舊空間(old-space)中指向新生代中對象的指針。咱們使用這個指針來維護從舊到新的引用列表,而不是跟蹤整個堆中的每個對象變動。當堆和全局對象結合使用時,咱們知道每個在新生代中對象的引用,而無需追蹤整個老生代。
而後將全部的活動對象移動到連續的一塊內存中,這樣作的好處就是徹底移除內存碎片(清理非活動對象時留下的內存碎片);而後咱們把兩塊內存空間互換,即把 ‘To-Space’ 變成 ‘From-Space’,反之亦然。一旦垃圾回收完成,新分配的內存空間將從 ‘From-Space’ 下一個空閒內存地址開始。
若是僅僅是憑藉這一策略,咱們就會很快的耗盡新生代的內存空間;爲了新生代的內存空間不被耗盡,在下一次垃圾回收的時候,咱們會把活動對象移動到老生代,而不是 ‘To-Space’。
清理的最後一步是把移動後的對象的指針地址更新,每個被複制對象都會留下一個轉發地址,用於更新指針以指向新的地址。
主垃圾回收器主要負責老生區中的垃圾回收。除了新生區中晉升的對象,一些大的對象會直接被分配到老生區。所以老生區中的對象有兩個特色,一個是對象佔用空間大,另外一個是對象存活時間長。
因爲老生區的對象比較大,若要在老生區中使用 Scavenge算法進行垃圾回收,複製這些大的對象將會花費比較多的時間,從而致使回收執行效率不高, 同時還會浪費一半的空間。於是,主垃圾回收器是採用標記 - 清除(Mark-Sweep)的算法進行垃圾回收的。下面咱們來看看該算法是如何工做的。
首先是標記過程階段。標記階段就是從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程當中,能到達的元素稱爲活動對象,沒有到達的元素就能夠判斷爲垃圾數據。
接下來就是垃圾的清除過程。它和副垃圾回收器的垃圾清除過程徹底不一樣,你能夠理解這個過程是清除掉紅色標記數據的過程,可參考下圖大體理解下其清除過程:
上面的標記過程和清除過程就是標記 - 清除算法,不過對一塊內存屢次執行標記 - 清除算法後,會產生大量不連續的內存碎片。而碎片過多會致使大對象沒法分配到足夠的連續內存,因而又產生了另一種算法——標記 - 整理(Mark-Compact)這個標記過程仍然與標記 - 清除算法裏的是同樣的,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。你能夠參考下圖
Orinoco 是 V8 垃圾回收器項目的代號,它利用最新的和最好的垃圾回收技術來下降主線程掛起的時間, 好比:並行(parallel)垃圾回收,增量(incremental)垃圾回收和併發(concurrent)垃圾回收。
並行是主線程和協助線程同時執行一樣的工做,可是這仍然是一種 ‘stop-the-world’ 的垃圾回收方式,可是垃圾回收所耗費的時間等於總時間除以參與的線程數量(加上一些同步開銷)。這是這三種技術中最簡單的 JavaScript 垃圾回收方式;由於沒有 JavaScript 的執行,所以只要確保同時只有一個協助線程在訪問對象就行了。
增量式垃圾回收是主線程間歇性的去作少許的垃圾回收的方式。咱們不會在增量式垃圾回收的時候執行整個垃圾回收的過程,只是整個垃圾回收過程當中的一小部分工做。作這樣的工做是極其困難的,由於 JavaScript 也在作增量式垃圾回收的時候同時執行,這意味着堆的狀態已經發生了變化,這有可能會致使以前的增量回收工做徹底無效。從圖中能夠看出並無減小主線程暫停的時間(事實上,一般會略微增長),只會隨着時間的推移而增加。但這仍然是解決問題的的好方法,經過 JavaScript 間歇性的執行,同時也間歇性的去作垃圾回收工做,JavaScript 的執行仍然能夠在用戶輸入或者執行動畫的時候獲得及時的響應。
併發是主線程一直執行 JavaScript,而輔助線程在後臺徹底的執行垃圾回收。這種方式是這三種技術中最難的一種,JavaScript 堆裏面的內容隨時都有可能發生變化,從而使以前作的工做徹底無效。最重要的是,如今有讀/寫競爭(read/write races),主線程和輔助線程極有可能在同一時間去更改同一個對象。這種方式的優點也很是明顯,主線程不會被掛起,JavaScript 能夠自由地執行 ,儘管爲了保證同一對象同一時間只有一個輔助線程在修改而帶來的一些同步開銷。
大部分 JavaScript 開發人員並不須要考慮垃圾回收,可是瞭解一些垃圾回收的內部原理,能夠幫助你瞭解內存的使用狀況,以及採起合適的編範式。
從上面的分析你也能看出來,不管是垃圾回收的策略,仍是處理Orinoco回收執行機制的策略,每每都沒有一個完美的解決方案,你須要花一些時間來作權衡,而這須要犧牲當前某幾方面的指標來換取其餘幾個指標的提高。