V8 工做原理

語言類型

  • 靜態語言:在使用以前就要肯定其變量類型的語言
  • 動態語言:在運行過程當中須要檢查變量類型的語言
  • 弱類型語言:支持隱式類型轉換的語言
  • 強類型語言:不支持隱式類型轉換的語言

Javascript 的內存模型

主要包含三部分:堆空間、棧空間、代碼空間。javascript

棧空間與堆空間

先看一段代碼:java

function foo() {
  var a = "極客時間";
  var b = a;
  var c = { name: "極客時間" };
  var d = c;
}
foo();
複製代碼

代碼存儲示意圖
從上圖能夠看出:

  1. 普通類型的變量都被保存在執行上下文中,執行上下文又被壓入到棧中。而引用類型的變量則是在棧中存放訪問堆空間的地址,其值被保存在堆空間中
  2. 原始類型賦值是複製變量值,引用類型是複製引用地址。
  • 爲何不都放在棧中?
  1. 棧空間小,主要用來存放一些原始類型的小數據;堆空間很大,能存放不少大數據,不過度配內存與回收內存都會佔用必定的時間。
  2. JS 引擎須要用棧來維護程序執行期間的上下文狀態,若過大,會影響上下文切換效率進而影響程序執行效率。

調用棧中切換執行上下文

垃圾數據是如何自動回收的?

JS 中的垃圾數據分爲:棧中的垃圾數據與堆中的垃圾數據算法

如下面這段代碼爲例分析:瀏覽器

function foo() {
  var a = 1;
  var b = { name: "極客邦" };
  function showName() {
    var c = "極客時間";
    var d = { name: "極客時間" };
  }
  showName();
}
foo();
複製代碼

執行到 showName 函數時的內存模型

棧中的垃圾回收

經過 ESP 指針(即記錄當前執行狀態的指針)來操做,ESP 指向 showName 的函數上下文時,表示當前正在執行 showName 函數。性能優化

當 showName 執行完畢後,ESP 下移指向 foo 的執行上下文,這個操做就是銷燬 showName 的函數執行上下文。網絡

下圖爲從棧中回收 showName 執行上下文的過程
架構

從棧中回收 showName 執行上下文

showName 雖然仍保留在棧中,但已屬於無效內存。當 foo 函數再次調用另一個函數時,showName 執行上下文會被覆蓋掉。

堆中的垃圾回收

當 foo 執行結束後,ESP 就指向全局上下文了,此時內存狀態以下圖。
函數

foo 執行結束後的內存狀態

從圖中能夠看出,堆中的垃圾並無被回收。 要想回收堆中的垃圾,就要用到 JS 的垃圾回收器了。 下面先介紹下垃圾回收領域的術語。

代際假說與分代收集

  • 代際假說的兩個特色:
  1. 大部分對象在內存中存在時間很短,即不少對象一經分配內存,很快就變得不可訪問。
  2. 不死的對象,會活的更久。

在 V8 中把堆分爲新生代老生代兩個區域,新生代存放生存時間很短的對象,容量只有 1~8M;老生代中存放生存時間久的對象,容量很大。性能

對應的就有兩種垃圾回收器,副垃圾回收器與主垃圾回收器。 主垃圾回收器:主要負責老生代的垃圾回收。 副垃圾回收器:主要負責新生代的垃圾回收。大數據

垃圾回收器的工做流程

兩種回收器有一套共同的執行流程。

  1. 標記空間中的活動對象與非活動對象。活動對象即還在使用的對象,非活動對象就是能夠進行垃圾回收的對象。
  2. 回收非活動對象佔用的內存。在全部對象標記完後,統一清理內存中被標記爲可回收的對象。
  3. 整理內存。頻繁回收會致使內存空間不連續,即有不少內存碎片。當要分配較大的連續內存時,就會出現內存不足的狀況;因此須要整理碎片。(副垃圾回收器不會產生內存碎片,故不須要這步。)

副垃圾回收器(GC:garbage collect)

主要用於回收新生代的垃圾,故內存不大。
下圖爲 V8 中的堆空間分佈。

V8的堆空間

新生代經過 Scavenge 算法將空間劃分爲對象區域與空閒區域。 每當對象區域被寫滿時,就會執行一次垃圾回收,具體過程以下:

  1. 先對對象區域中的垃圾作標記;
  2. 標記完成後,GC 將非活動的對象回收,將存活的對象複製到空閒區域中,同時將對象進行有序排列。(至關於內存整理)
  3. 完成複製後,再將對象區域與空閒區域進行反轉,這樣就完成了垃圾回收。

因爲複製操做須要時間成本且操做頻繁,因此爲了執行效率,新生代的空間都不會太大;若通過兩次 GC 回收依然存活,就會將活着的對象移到老生代中,這就是 JS 引擎的對象晉升策略

主垃圾回收器

主要用於回收老生代的垃圾,除了新生代中晉升的對象,一些大的對象會被直接分配到老生代。

老生代對象的兩個特色:

  1. 佔用空間大
  2. 存活時間長

基於上述兩個特色,主垃圾回收器採用標記-清除(Mark-Sweep)的算法進行垃圾回收。

標記:從一組根元素開始,遞歸遍歷這組根元素,能到達的元素成爲活動對象,沒有到達的爲垃圾數據

function foo() {
  var a = 1;
  var b = { name: "極客邦" };
  function showName() {
    var c = "極客時間";
    var d = { name: "極客時間" };
  }
  showName();
}
foo();
複製代碼

仍是這段代碼,當 showName 函數退出後,調用棧和堆空間以下圖:

標記過程

從上圖能夠看出,當 showName 執行結束後,ESP 執行 foo 的執行上下文,此時遍歷調用棧,不會找到引用 1003 地址的變量,意味着 1003 這塊數據爲垃圾數據,被標記爲紅色;而 1050 被 b 引用了,因此會被標記爲活動對象。

下圖爲垃圾清除的過程

標記清除過程

對一塊內存屢次執行標記-清除算法後,會產生大量不連續的內存碎片。因而又出現了另外一種算法標記-整理(Mark-Compact),標記過程都是同樣的,標記完後將全部的活動對象都向一端移動,而後直接清除掉端邊界之外的內存。

標記整理過程

全停頓與增量標記

全停頓:JS 是運行在主線程之上的,一旦執行垃圾回收,就須要將正在執行的 JS 腳本暫停下來,待垃圾回收完畢後再恢復腳本執行,這期間應用的性能和響應能力都會直線降低。這個過程就叫作全停頓(Stop-The-World)。

全停頓

V8 新生代垃圾回收由於空間小,存活對象少,全停頓影響不大;但老生代就不同了,好比正在執行 JS 動畫,GC 工做致使主線程不能作其餘事情,那個動畫在這段時間沒法執行,頁面就會卡頓。

爲下降卡頓,因而誕生了增量標記(Incremental Marking)算法。 增量標記:V8 將標記過程分爲一個個子標記過程,同時讓垃圾回收器和 JS 應用邏輯交替進行,直至標記完成。這些小任務執行時間短,穿插再 JS 任務間執行,這樣就不會感覺到頁面卡頓了。

增量標記

編譯器(TurboFan)與解釋器(Ignition)

  • 編譯型語言
    在程序執行前,需通過編譯器編譯,而且編譯以後會直接保留機器能讀懂的二進制文件;之後每次運行程序時,直接運行二進制文件,不須要從新編譯。

  • 解釋型語言 在每次運行時都要通過解釋器對程序進行動態解釋和執行。

編譯器與解釋器
上圖中,編譯器若是編譯成功,會生成一個可執行文件;若編譯過程有語法或其餘錯誤,編譯器就會拋出異常,二進制文件也不會生成。解釋器是根據生成的字節碼來執行程序,輸出結果。

V8 如何執行一段 JS 代碼?

V8 執行代碼流程圖

結合上圖,V8 執行代碼可分爲如下幾步

生成 AST 和執行上下文

將源代碼轉換爲編譯器和解釋器能夠理解的 AST,並生成執行上下文。

  • 爲何要生成 AST 以及什麼是 AST?
    先說第一個,由於編譯器和解釋器沒法理解高級語言,因此要生成 AST,就像渲染引擎將 HTML 轉換爲計算機能夠理解的 DOM 樹同樣。

    再來講第二個,AST 是代碼的結構化表示,有着很是重要的做用。
    Babel 原理: 將 ES6 源碼轉換爲 AST,再將 ES6 語法的 AST 轉換爲 ES5 語法的 AST,最後利用它來生成 ES5 源代碼。
    ESLint 原理:檢測流程也是將源碼轉換爲 AST,再利用 AST 來檢查代碼規範。

  • 如何生成 AST?
    兩個階段:分詞(詞法分析),解析(語法分析)。

    詞法分析:將源碼拆解成最小的、不可再分的 token(關鍵字、標識符、賦值、字符串)。

    分解token

    語法分析:根據規則將上一步的 token 轉換爲 AST。若源碼存在語法錯誤,則不會生成 AST。

有了 AST 之後,V8 就會生成這段代碼的執行上下文。

如何查看代碼生成的 AST

javascript-ast

生成字節碼

有了 AST 和執行上下文後,解釋器根據 AST 生成字節碼並解釋執行。

  • 爲何不直接轉成機器碼,而是經過字節碼轉成機器碼? 看下圖:

    字節碼&機器碼

    由上圖能夠看出,機器碼佔用的空間遠大於字節碼,早期手機內存只有 512M,而 V8 須要消耗大量內存來存放轉換後的機器碼,爲了解決內存佔用問題,就有了如今這套架構。

字節碼:介於 AST 與機器碼之間的一種代碼,須要經過解釋器將其轉爲機器碼後才能執行。

執行代碼

第一次執行字節碼時,解釋器會逐條解釋執行,若是發現有熱點代碼(即一段代碼被重複執行屢次),編譯器就會將這段熱點代碼編譯爲機器碼保存起來,當再次執行這段代碼時,只須要執行編譯後的機器碼;這種技術就叫作即時編譯(JIT)

JIT 技術

JS 性能優化

將優化的中心聚焦在單次腳本執行時間和腳本的網絡下載上。

  1. 提高單次腳本的執行速度,避免 JavaScript 的長任務霸佔主線程,這樣可使得頁面快速響應交互;
  2. 避免大的內聯腳本,由於在解析 HTML 的過程當中,解析和編譯也會佔用主線程;
  3. 減小 JavaScript 文件的容量,由於更小的文件會提高下載速度,佔用更低的內存。

參考資料

瀏覽器工做原理與實踐

相關文章
相關標籤/搜索