這一次,完全弄清楚V8垃圾回收的流程

本人在面試候選人的時候,即便一個剛畢業的前端,問他 javascript 中內存的分配,都能答出來棧內存、堆內存。可是再追問一下,堆內存到底是怎麼分配的,80% 的面試者都回答不上來了。javascript

V8 對內存的分配

都知道,js對象是存放在堆內存中的,那麼具體是放在哪裏呢?前端

V8 引擎會把內存中的 堆內存 分爲兩塊不一樣的區域,一塊稱之爲老生代(old generation),另外一塊是新生代(young generation)java

即便同處 新生代 中的對象中,它們的等級也不一樣,又進一步分爲 初級(nursery)代 等級和 中級(intermediate)代 等級。程序員

能夠類比紅警這類遊戲中,剛出生的美國大兵是一級兵,經歷過一場激烈的戰役以後,倖存的大兵會被提高到二級兵,再經歷一場戰役,會被提高到三級兵。面試

js 的初級代、中級代也是如此。編程

在 js 中,當一個對象第一次分配內存時,會被分配到 新生代 中的 初級(nursery)代,至關因而最弱雞的一級兵。瀏覽器

這個對象,若是在第一輪的垃圾回收中倖存下來。那麼,咱們把它的等級到 中級(intermediate)代,也就是晉升成爲了 二級兵。多線程

若是再通過下一次垃圾回收,這個對象倖存下來,這時候咱們就會把這個對象,從中級(intermediate)代移動到老生代,也就是晉升成爲了三級兵。併發

爲啥 V8 要這麼作呢?編程語言

在垃圾回收中有一個重要的概念:「代際假說」(The Generational Hypothesis)。就是說,大部分的 js 對象,都是炮灰,一輪垃圾回收後,基本上都不會倖存,在內存中存在的時間很短。換句話說,從垃圾回收的角度來看,不少對象一經分配內存空間隨即就變成了不可訪問的。如同下圖所示:

既然,短命的js對象,和命久的 js對象有如此的差距,V8 中就把他們區分開,採用不一樣的垃圾回收策略。

javascript 主線程在正常的執行的時候,佔用的內存空間會不斷的增加。增加就會觸發一個極限,觸發極限的時候,垃圾回收就被觸發了。對於新生代和老生代, V8 分別有兩種垃圾回收器去處理。

V8中兩種垃圾回收器

V8 有兩個垃圾回收器,一個是主垃圾回收器(Full Mark-Compact),一個是副垃圾回收器( Scavenge )。這兩個垃圾回收器,是相互獨立的。

主垃圾回收器主要負責老生區中的垃圾回收(也會負責一部分的新生代heap),副垃圾回收器重新生代中回收垃圾。

From-Space / To-Space

對於副垃圾回收器來講,有兩塊內存空間比較相關: From-spaceTo-space

不要感到困惑,From-spaceTo-space 的概念,和 初級代中級代 的概念,不是同一個概念。

From-spaceTo-space 能夠理解成,真實可操做的內存空間;初級代中級代 表示一個對象的等級。From-SpaceTo-Space 永遠是一個是空的,一個是使用中的。即便同處在 From-Space 中的對象,有的對象多是初級代(一等兵),有的對象多是中級代(二級兵)。

接下來,介紹一下,副垃圾回收器的過程。

副垃圾回收器 步驟

第1步 打標

這一步,是爲了判斷這輪GC中哪些對象須要被回收。

如何判斷呢?就是看這個對象能不能被找到。

打標首先從根部開始查找,也就是頂層的執行棧、全局的對象開始查找,而後查找對象的引用,而後是對象引用的引用,一層層遞歸的找。

若是一個對象能夠被訪問到,則認爲這個對象是活的,不該該被回收;不然,就會被回收。

接下來,咱們開始一輪垃圾回收的過程。

第2步

第 2 步,V8 從 from-space 中,把那些不會被垃圾清理掉的對象,移動到 to-space。這意味着,只有小部分的對象不會被垃圾清理掉,成功撤離到 to-space。 剩在 from-space 中的大部分對象,當成炮灰銷燬。

不要和 js 代碼中的拷貝對象指針搞混了,這裏是更底層的二進制數據。拷貝的過程,就是拷貝那一塊內存中的二進制數據的過程。

這一步也稱爲 撤離步驟 evacuation step,這步結束後,內存中的狀況以下圖:

上圖右側,撤離到 to-space 中的對象,成功的在第一輪的垃圾回收中活了下來,給他們的小方塊打上一個標記(也就是每一塊的小圓圈),標誌着它們從 初級代 晉升到了 中級代

第3步

接下來,第3步被稱爲更新引用指針。

咱們發現,js 中對象的引用指針,仍是引用到了舊的from-space 空間上,咱們須要更新這些引用到 to-space 空間上。

結束後,以下圖所示:

第4步

接下來,咱們把 to-spacefrom-space 交換位置。to-space 移動到左側,成爲了 下一輪的 from-spacefrom-space 移動到右側,成爲了下一輪的 to-space

第二輪垃圾回收

第一輪垃圾回收以後, js 繼續執行,會有一些新分配的 初級代 對象,被推入到了 from-space 空間中, 安置在上一輪的倖存老兵 中級代 對象後面,以下圖紅色箭頭所示:

第二輪垃圾回收的過程,和第一輪相似,就不贅述了。

二輪的垃圾回收的關鍵點是:from-space 中的倖存老兵 中級代 會拷貝最右側的 old generation , 晉升爲 老生代 。剛加入的新兵 初級代 會被拷貝到 to-space , 也打上一個標誌,晉升成爲 中級代, 以下圖所示:

副垃圾回收器 與 併發處理

現今,V8 在新生代垃圾回收中使用併發清理。

什麼? 併發?

對,你沒有聽錯。雖然 javascript 是單線程的語言,這僅僅意味着 javascript 的程序員寫的代碼大部分是在 單線程上面跑的。可是 javascript 語言的宿主環境,好比說 V8 引擎,它是 Javascript 的執行環境,它能夠新建出不少線程出來,用來輔助 javascript 主線程的工做。咱們把這些其餘的輔助的線程稱爲 輔助線程(helper), javascript 執行的線程是 主線程(main thread)

把倖存對象撤離到 to-space 的工做,是 主線程 和 輔助線程一塊兒併發執行,是爲了d最大限度的減小 GC 的時間。

  • 每一個輔助線程 和 主線程,會把活的對象都移動到 To-Space。在每一次嘗試將活的對象移動到 To-Space 的時,必須確保原子操做。

  • 不一樣的輔助線程,都有可能經過不一樣的路徑找到相同的對象,並嘗試將這個對象移動到 To-Space;不管哪一個輔助線程成功移動對象到 To-Space ,都必須更新這個對象的指針。

副垃圾回收器 小結

  • 由於代際假說的理論,只有小部分的 js 對象是會倖存下來的,因此在副垃圾回收器中,只會撤離一小部分的對象,拷貝到to-space的空間中,其餘大部分對象都通通銷燬。

  • from-spaceto-space 只有一個在用,空間開銷很大,典型的用空間換時間。

  • 輔助線程 併發的幫助撤離

主垃圾回收器

上文中提到,新生代的對象,若是連續二輪GC倖存,會被晉升到老生代。

接下來,咱們來看一下,老生代的對象是如何被 主垃圾回收器所處理的。

老生代的垃圾回收會經歷下面幾個過程:打標 ( marking ) 清掃 (sweeping) **、壓縮 (compacting)

打標

打標的過程, 在上文副垃圾回收器中已經講過了

清掃

打標以後,V8 知道有哪些對象是不會被訪問到,也就是須要被回收的了。這些被回收的對象所佔用的位置,人走茶涼,就空了下來,成爲了一個空閒的位置。

V8 會管理這些空閒的位置,以便下次有新到對象來了,能夠把新到對象安置在空間位置中。

V8 把這些空閒的位置,掃到一張叫 FreeList 的表中來記錄,這個過程被稱爲清掃,清掃的過程可讓一個輔助線程在後臺靜默的去作掉。

壓縮

若是你瞭解計算機操做系統,必定了解 碎片 的概念。

壓縮的意思是,咱們想把 內存中的數據,擠一擠,靠得緊湊一點,把他們中間的間隙——也就是碎片 ,合併成一個大一些的連續空間。這樣下次來一個比較大的對象時,能夠有充足的空間來存放。

輔助線程 併發處理

在主垃圾回收器中,一樣存在多輔助線程來提高效率。

首先, 輔助線程 開始併發的去打標(marking)

接下來, 當 輔助線程(helper) 的工做作完了,主線程就會暫停執行,轉而進行最後的打標記工做(finalize marking)開始清理工做 ( sweeping tasks )

最後的打標記工做是主線程會快速的從根部從新檢查一下,看有 輔助線程 是否有遺漏的,確保全部的對象都正確的掃過了

若是檢查完畢OK,主線程和一部分輔助線程齊心合力一塊兒作 合併碎片(Compact)更新(update)的操做。

另一部分輔助線程,會去併發的執行 清掃 工做,並不會影響並行內存頁的整理工做和 JavaScript 的執行。

當這些工做都作完了,主線程會從新開始執行代碼。

空閒時垃圾回收器

對於 JavaScript 程序員來講,咱們是沒有辦法直接操做垃圾回收器的。

爲了解決這個問題, V8 提出了空閒時間的概念。咱們的頁面跑在瀏覽器內,瀏覽器以每秒60幀的速度去執行一些動畫,瀏覽器大約有16.6毫秒的時間去渲染動畫的每一幀。

若是這些渲染的工做,提早完成了,那麼瀏覽器在下一幀以前的空閒時間去觸發垃圾回收器。

總結

V8 的垃圾回收器項目自立項以來已經走過了漫長的道路。向現有的垃圾回收器添加並行、併發和增量垃圾回收技術通過了不少年的努力,而且也已經取得了一些成效。

將大量的移動對象的任務轉移到後臺進行,大大減小了主線程暫停的時間,改善了頁面卡頓,讓動畫,滾動和用戶交互更加流暢。Scavenger 回收器將新生代的垃圾回收時間減小了大約 20% - 50%,空閒時垃圾回收器在 Gmail 網頁應用空閒的時候將 JavaScript 堆內存減小了 45%。併發標記清理能夠減小大型 WebGL 遊戲的主線程暫停時間,最多能夠減小 50%。

大部分 JavaScript 開發人員並不須要考慮垃圾回收,可是瞭解一些垃圾回收的內部原理,能夠幫助你瞭解內存的使用狀況,以及採起合適的編範式。好比:從 V8 堆內存的分代結構和垃圾回收器的角度來看,建立生命週期較短的對象的成本是很是低的,可是對於生命週期較長的對象來講成本是比較高的。這些模式是適用於不少動態編程語言的,而不只僅是 JavaScript。

相關文章
相關標籤/搜索