V8 引擎垃圾回收與內存分配

這是第 82 篇不摻水的原創,想獲取更多原創好文,請搜索公衆號關注咱們吧~ 本文首發於政採雲前端博客: V8 引擎垃圾回收與內存分配

寫在前面

工欲善其事,必先利其器,本文之器非器具之器,乃容器也,言歸正傳,做爲一個前端打工人,左手剛 const 定義常量,忠貞不二,轉頭就 new 幾個對象,玩的火熱,真是個優秀的 jser,風騷的操做背後,必有日夜不輟的 QWER,外加一個走 A,廢話很少說,瀏覽器內核是啥玩意?還不知道都有啥瀏覽器內核?那就先來看看瀏覽器內核。前端

瀏覽器內核

提到瀏覽器內核,Blink、Weikit、Gecko、Trident 張口就來,這些只是各個瀏覽器內核的組成部分之一渲染引擎,對應的還有 JavaScript引擎,簡單羅列一下:算法

瀏覽器 渲染引擎 Javascript 引擎
Chrome Blink(13 年以前使用的是 Safari 的 Webkit, Blink 是谷歌與歐朋一塊兒搞的) V8
Safari Webkit JavaScriptCore
Firefox Gecko SpiderMonkey--OdinMonkey
IE Trident Chakra

渲染引擎和 JS 引擎相互協做,打造出瀏覽器顯示的頁面,看下圖:npm

圖片

簡單看看就行,不重要,既然是講垃圾回收( Garbage Collection 簡稱 GC ),那就要先去回收站了,回收站有個學名叫:內存,計算機五大硬件之一存儲器的核心之一,見下圖:瀏覽器

圖片

說句更不重要的,JS 是沒有能力管理內存和垃圾回收的,一切都要依賴各個瀏覽器的 JS 引擎,因此爲了逼格更高一點,就不要說 JS 垃圾回收了,你看,我說 V8 垃圾回收,是否是厲害多了(摸了摸愈來愈沒有阻力的腦殼)。緩存

內存分配

簡單說,棧內存,小且存儲連續,操做起來簡單方便,通常由系統自動分配,自動回收,因此文章內所說的垃圾回收,都是基於堆內存。併發

堆內存,大(相對棧來講)且不連續。app

V8 中內存分類

在講內存分配以前,先了解一下弱分代假說,V8 的垃圾回收主要創建在這個假說之上。ide

概念:post

  • 絕大部分的對象生命週期都很短,即存活時間很短
  • 生命週期很長的對象,基本都是常駐對象

基於以上兩個概念,將內存分爲新生代 (new space)老生代 (old space)兩個區域。劃重點,記一下。性能

垃圾回收

新生代

新生代(32 位系統分配 16M 的內存空間,64 位系統翻倍 32M,不一樣瀏覽器可能不一樣,可是應該差不了多少)。

新生代對應存活時間很短的假說概念,這個空間的操做,很是頻繁,絕大多數對象在這裏經歷一次生死輪迴,基本消亡,沒消亡的會晉升至老生代內。

新生代算法爲 Scavenge 算法,典型犧牲空間換時間的敗家玩意,怎麼說呢?首先他將新生代分爲兩個相等的半空間( semispace ) from spaceto space,來看看這個敗家玩意,是怎麼操做的,他使用寬度優先算法,是寬度優先,記住了不。兩個空間,同一時間內,只會有一個空間在工做( from space ),另外一個在休息( to space )。

  1. 首先,V8 引擎中的垃圾回收器檢測到 from space 空間快達到上限了,此時要進行一次垃圾回收了
  2. 而後,從根部開始遍歷,不可達對象(即沒法遍歷到的對象)將會被標記,而且複製未被標記的對象,放到 to space 中
  3. 最後,清除 from space 中的數據,同時將 from space 置爲空閒狀態,即變成 to space,相應的 to space 變成 from space,俗稱翻轉

圖片

也是,你說空間都給他了,他愛咋地處理就咋地處理唄,總不可能強迫王校長開二手奧拓吧,固然了,對於小對象,這麼來一次,時間的優點那是槓槓的,雖然浪費了一半空間,可是問題不大,能 hold 住。

固然優秀的 V8 是不可能容忍,一個對象來回的在 form space 和 to space 中蹦躂的,當經歷一次 form => to 翻轉以後,發現某些未被標記的對象竟然還在,會直接扔到老生代裏面去,好似後浪參加比賽,晉級了,優秀的嘞。

除了上面一種狀況,還有一個狀況也會晉級,當一個對象,在被複制的時候,大於 to space 空間的 25% 的時候,也會晉級了,這種自帶背景的選手,那是不敢動的,直接晉級到老生代。

老生代

老生代( 32 位操做系統分配大約 700M 內存空間,64 位翻倍 1.4G,同樣,每一個瀏覽器可能會有差別,可是差不了多少)。

老生代比起新生代但是要複雜的多,所謂能者多勞,空間大了,責任就大了,老生代能夠分爲如下幾個區域:

  • old object space 即你們口中的老生代,不是所有老生代,這裏的對象大部分是由新生代晉升而來
  • large object space 大對象存儲區域,其餘區域沒法存儲下的對象會被放在這裏,基本是超過 1M 的對象,這種對象不會在新生代對象中分配,直接存放到這裏,固然了,這麼大的數據,複製成本很高,基本就是在這裏等待命運的降臨不可能接受僅僅是知其然,而不知其因此然
  • Map space 這個玩意,就是存儲對象的映射關係的,其實就是隱藏類,啥是隱藏類?就不告訴你(不知道的大佬已經去百度了)
  • code space 簡單點說,就是存放代碼的地方,編譯以後的代碼,是根據大佬們寫的代碼編譯出來的代碼

看個圖,休息一下:

圖片

講了這麼多基本概念,聊聊最後的老生代回收算法,老生代回收算法爲:標記和清除/整理(mark-sweep/mark-compact)。

在標記的過程當中,引入了概念:三色標記法,三色爲:

  • 白:未被標記的對象,即不可達對象(沒有掃描到的對象),可回收
  • 灰:已被標記的對象(可達對象),可是對象尚未被掃描完,不可回收
  • 黑:已被掃描完(可達對象),不可回收

固然,既然要標記,就須要提供記錄的坑位,在 V8 中分配的每個內存頁中建立了一個 marking bitmap 坑位。

大體的流程爲:

  1. 首先將全部的非根部對象所有標記爲白色,而後使用深度優先遍歷,是深度優先哈,和新生代不同哈,按深度優先搜索沿途遍歷,將訪問到的對象,直接壓入棧中,同時將標記結果放在 marking bitmap (灰色) 中,一個對象遍歷完成,直接出棧,同時在 marking bitmap 中記錄爲黑色,直到棧空爲止,來張圖,休息一下

圖片

  1. 標記完成後,接下來就是等待垃圾回收器來清除了,清除完了以後,會在原來的內存區域留下一大堆不連續的空間,小對象還好說,這個時候若是來一個稍微大一點的對象,沒有內存能夠放的下這個傻大個了,怎麼辦?只能觸發 GC,可是吧,原來清除的不連續的空間加起來又能夠放的下這個傻大個,很惋惜啊,啓動一次 GC 性能上也是嗖嗖的往下掉啊;V8 能允許這樣的事發生?確定不存在嘛!
  2. 因此在清除完以後,新生代中對象,再一次分配到老生帶而且內存不足的時候,會優先觸發標記整理(mark-compact), 在標記結束後,他會將可達對象(黑色),移到內存的另外一端,其餘的內存空間就不會被佔用,直接釋放,等下次再有對象晉升的時候,輕鬆放下。

看到這裏各位大佬可能會有疑問,那要是我 GC 搞完以後,再來個對象,滿了咋辦,你說咋辦,直接崩好很差,這個時候就須要大佬們寫代碼的時候,要珍惜內存了,對內存就像珍惜你的女友同樣,啥?沒有女友? 那就沒辦法了,原則上是決不了這個問題的。

基本的內存和垃圾回收是交代完了,其中還有一些概念,仍是要說一下的,接着往下看!

圖片

寫屏障

想一個問題,當 GC 想回收新生代中的內容的時候,某些對象,只有一個指針指向了他,好巧不巧的是,這個指針仍是老生代那邊對象指過來的,怎麼搞?我想回收這個玩意,難道要遍歷一下老生代中的對象嗎?這不是開玩笑嗎?爲了回收這一個玩意,我須要遍歷整個老生代,代價着實太大,搞不起,搞不起,那怎麼辦哩?

V8 引擎中有個概念稱做寫屏障,在寫入對象的地方有個緩存列表,這個列表內記錄了全部老生代指向新生代的狀況,固然了新生成的對象,並不會被記錄,只有老生代指向新生代的對象,纔會被寫入這個緩存列表。

在新生代中觸發 GC 遇到這樣的對象的時候,會首先讀一下緩存列表,這相比遍歷老生代全部的對象,代價實在是過小了,這操做值得一波 666,很優秀,固然了,關於 V8 引擎內在的優化,還有不少不少,各位大佬能夠慢慢去了解。

全停頓(stop-the-world)

關於全停頓,本沒有必要單獨來說,可是,I happy 就 good。

在以往,新/老生帶都包括在內,爲了保證邏輯和垃圾回收的狀況不一致,須要中止 JS 的運行,專門來遍歷去遍歷/複製,標記/清除,這個停頓就是:全停頓。

這就比較噁心了,新生代也就算了,自己內存不大,時間上也不明顯,可是在老生代中,若是遍歷的對象太多,太大,用戶在此時,是有可能明顯感到頁面卡頓的,體驗嘎嘎差。

因此在 V8 引擎在名爲 Orinoco 項目中,作了三個事情,固然只針對老生代,新生代這個後浪仍是能夠的,效率賊拉的高,優化空間不大。三個事情分別是:

  • 增量標記

將原來一口氣去標記的事情,作成分步去作,每次內存佔用達到必定的量或者屢次進入寫屏障的時候,就暫時中止 JS 程序,作一次最多幾十毫秒的標記 marking,當下次 GC 的時候,反正前面都標記好了,開始清除就好了

  • 並行回收

從字面意思看並行,就是在一次全量垃圾回收的過程當中,就是 V8 引擎經過開啓若干輔助線程,一塊兒來清除垃圾,能夠極大的減小垃圾回收的時間,很優秀,手動點贊

  • 併發回收

併發就是在 JS 主線程運行的時候,同時開啓輔助線程,清理和主線程沒有任何邏輯關係的垃圾,固然,須要寫屏障來保障

小結

V8 引擎作的優化有不少,還有好比屢次( 2 次)在新生代中可以存活下來的對象,會被記錄下來,在下次 GC 的時候,會被直接晉升到老生代,還有好比新晉升的對象,直接標記爲黑色,這是由於新晉升的對象存活下來的機率很是高,這兩種狀況就算是再也不使用,再下下次的時候也會被清除掉,影響不大,可是這個過程,第一種就省了新生代中的一次複製輪迴,第二種就省了 marking 的過程,在此類對象比較多的狀況下,仍是比較有優點的。

最後一句

終於,寫完了,原本想着寫的更詳細一些,可是那樣篇幅會很大,下次吧,有機會的話再寫寫 V8 執行的過程或者 V8 建立對象都幹了些啥玩意什麼什麼的,其實 V8 引擎(或者各個 JS 引擎)這個東西太龐大了,我瞭解的也是冰山一角,因此文章確定有不許確的地方,歡迎大佬們嚴正指正,積極交流。

推薦閱讀

初級工程師如何快速成長和尋求突破

npm 私庫從搭建到數據遷移最後容災備份的一些解決方案

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 40 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索