最近看到一篇文章,詳細講述了瀏覽器是如何工作的,感覺非常好,所以決定一點點摘錄及研究下。
V8 是由 Google 開發的開源 JavaScript 引擎,也被稱為虛擬機,模擬實際計算機各種功能來實現代碼的編譯和執行。

一、為什么需要 JavaScript 引擎
我們寫的 JavaScript 代碼直接交給瀏覽器或者 Node 執行時,底層的 CPU 是不認識的,也沒法執行。CPU 只認識自己的指令集,指令集對應的是匯編代碼。寫匯編代碼是一件很痛苦的事情。並且不同類型的 CPU 的指令集是不一樣的,那就意味着需要給每一種 CPU 重寫匯編代碼。
JavaScirpt 引擎可以將 JS 代碼編譯為不同 CPU(Intel, ARM 以及 MIPS 等)對應的匯編代碼,這樣我們就不需要去翻閱每個 CPU 的指令集手冊來編寫匯編代碼了。當然,JavaScript 引擎的工作也不只是編譯代碼,它還要負責執行代碼、分配內存以及垃圾回收。
# 將一個寄存器中的數據移動到另外一個寄存器中 1000100111011000 #機器指令 mov ax,bx #匯編指令
1、熱門 JavaScript 引擎
- V8 (Google),用 C++編寫,開放源代碼,由 Google 丹麥開發,是 Google Chrome 的一部分,也用於 Node.js。
- JavaScriptCore (Apple),開放源代碼,用於 webkit 型瀏覽器,如 Safari ,2008 年實現了編譯器和字節碼解釋器,升級為了 SquirrelFish。蘋果內部代號為“Nitro”的 JavaScript 引擎也是基於 JavaScriptCore 引擎的。
- Rhino,由 Mozilla 基金會管理,開放源代碼,完全以 Java 編寫,用於 HTMLUnit
- SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用於 Netscape Navigator,現時用於 Mozilla Firefox。
- Chakra (JScript 引擎),用於 Internet Explorer。
- Chakra (JavaScript 引擎),用於 Microsoft Edge。
- KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波頓開發,用於 KDE 項目的 Konqueror 網頁瀏覽器中。
- JerryScript — 三星推出的適用於嵌入式設備的小型 JavaScript 引擎。
- 其他:Nashorn、QuickJS 、 Hermes
2、V8
Google V8 引擎是用 C ++編寫的開源高性能 JavaScript 和 WebAssembly 引擎,它已被用於 Chrome 和 Node.js 等。可以運行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 處理器的 Linux 系統上。
V8 最早被開發用以嵌入到 Google 的開源瀏覽器 Chrome 中,第一個版本隨着第一版Chrome於 2008 年 9 月 2 日發布。但是 V8 是一個可以獨立運行的模塊,完全可以嵌入到任何 C ++應用程序中。著名的 Node.js( 一個異步的服務器框架,可以在服務端使用 JavaScript 寫出高效的網絡服務器 ) 就是基於 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。
和其他 JavaScript 引擎一樣,V8 會編譯 / 執行 JavaScript 代碼,管理內存,負責垃圾回收,與宿主語言的交互等。通過暴露宿主對象 ( 變量,函數等 ) 到 JavaScript,JavaScript 可以訪問宿主環境中的對象,並在腳本中完成對宿主對象的操作。

資料拓展:v8 logo | V8 (JavaScript engine)) | 《V8、JavaScript+的現在與未來》 | 幾張圖讓你看懂 WebAssembly
V8一詞最早見於“V-8 engine”,即V8發動機,一般使用在中高端車輛上。8個氣缸分成兩組,每組4個,成V型排列。是高層次汽車運動中最常見的發動機結構,尤其在美國,IRL,ChampCar和NASCAR都要求使用V8發動機。
3、什么是 D8
d8 是一個非常有用的調試工具,你可以把它看成是 debug for V8 的縮寫。我們可以使用 d8 來查看 V8 在執行 JavaScript 過程中的各種中間數據,比如作用域、AST、字節碼、優化的二進制代碼、垃圾回收的狀態,還可以使用 d8 提供的私有 API 查看一些內部信息。
V8源碼編譯出來的可執行程序名為d8。d8作為V8引擎在命令行中可以使用的交互shell存在。Google官方已經不記得d8這個名字的由來,但是作為"delveloper shell"的縮寫,用首字母d和8結合,恰到好處。
還有一種說法是d8最初叫developer shell,因為d后面有8個字符,因此簡寫為d8,類似於i18n(internationalization)這樣的簡寫。
參考:Using d8
二、V8 引擎的內部結構
V8 是一個非常復雜的項目,有超過 100 萬行 C++代碼。它由許多子模塊構成,其中這 4 個模塊是最重要的:
1、Parser:負責將 JavaScript 源碼轉換為 Abstract Syntax Tree (AST)
確切的說,在“Parser”將 JavaScript 源碼轉換為 AST前,還有一個叫”Scanner“的過程,具體流程如下:

2、Ignition:interpreter,即解釋器
負責將 AST 轉換為 Bytecode,解釋執行 Bytecode;同時收集 TurboFan 優化編譯所需的信息,比如函數參數的類型;解釋器執行時主要有四個模塊,內存中的字節碼、寄存器、棧、堆。
通常有兩種類型的解釋器,基於棧 (Stack-based)和基於寄存器 (Register-based),
基於棧的解釋器使用棧來保存函數參數、中間運算結果、變量等;
基於寄存器的虛擬機則支持寄存器的指令操作,使用寄存器來保存參數、中間計算結果。
通常,基於棧的虛擬機也定義了少量的寄存器,基於寄存器的虛擬機也有堆棧,其區別體現在它們提供的指令集體系。大多數解釋器都是基於棧的,比如 Java 虛擬機,.Net 虛擬機,還有早期的 V8 虛擬機。基於堆棧的虛擬機在處理函數調用、解決遞歸問題和切換上下文時簡單明快。而現在的 V8 虛擬機則采用了基於寄存器的設計,它將一些中間數據保存到寄存器中。
基於寄存器的解釋器架構:

資料參考:解釋器是如何解釋執行字節碼的?
3、TurboFan:compiler,即編譯器,
利用 Ignition 所收集的類型信息,將 Bytecode 轉換為優化的匯編代碼;
4、Orinoco:garbage collector,垃圾回收模塊
負責將程序不再需要的內存空間回收。
其中,Parser,Ignition 以及 TurboFan 可以將 JS 源碼編譯為匯編代碼,其流程圖如下:

簡單地說,Parser 將 JS 源碼轉換為 AST,然后 Ignition 將 AST 轉換為 Bytecode,最后 TurboFan 將 Bytecode 轉換為經過優化的 Machine Code(實際上是匯編代碼)。
- 如果函數沒有被調用,則 V8 不會去編譯它。
- 如果函數只被調用 1 次,則 Ignition 將其編譯 Bytecode 就直接解釋執行了。TurboFan 不會進行優化編譯,因為它需要 Ignition 收集函數執行時的類型信息。這就要求函數至少需要執行 1 次,TurboFan 才有可能進行優化編譯。
- 如果函數被調用多次,則它有可能會被識別為熱點函數,且 Ignition 收集的類型信息證明可以進行優化編譯的話,這時 TurboFan 則會將 Bytecode 編譯為 Optimized Machine Code(已優化的機器碼),以提高代碼的執行性能。
圖片中的紅色虛線是逆向的,也就是說Optimized Machine Code 會被還原為 Bytecode,這個過程叫做 Deoptimization。這是因為 Ignition 收集的信息可能是錯誤的,比如 add 函數的參數之前是整數,后來又變成了字符串。生成的 Optimized Machine Code 已經假定 add 函數的參數是整數,那當然是錯誤的,於是需要進行 Deoptimization。
function add(x, y) { return x + y; } add(3, 5); add('3', '5');
在運行 C、C++以及 Java 等程序之前,需要進行編譯,不能直接執行源碼;但對於 JavaScript 來說,我們可以直接執行源碼(比如:node test.js),它是在運行的時候先編譯再執行,這種方式被稱為即時編譯(Just-in-time compilation),簡稱為 JIT。因此,V8 也屬於 JIT 編譯器。
資料拓展參考:V8 引擎是如何工作的?
三、V8 是怎么執行一段 JavaScript 代碼的
1、在 V8 出現之前,所有的 JavaScript 虛擬機所采用的都是解釋執行的方式,這是 JavaScript 執行速度過慢的一個主要原因。
而 V8 率先引入了即時編譯(JIT)的雙輪驅動的設計(混合使用編譯器和解釋器的技術),這是一種權衡策略,混合編譯執行和解釋執行這兩種手段,給 JavaScript 的執行速度帶來了極大的提升。
V8 出現之后,各大廠商也都在自己的 JavaScript 虛擬機中引入了 JIT 機制,所以目前市面上 JavaScript 虛擬機都有着類似的架構。另外,V8 也是早於其他虛擬機引入了惰性編譯、內聯緩存、隱藏類等機制,進一步優化了 JavaScript 代碼的編譯執行效率。
2、V8 執行一段 JavaScript 的流程圖:

3、V8 本質上是一個虛擬機,因為計算機只能識別二進制指令,所以要讓計算機執行一段高級語言通常有兩種手段:
- 第一種是將高級代碼轉換為二進制代碼,再讓計算機去執行;
- 另外一種方式是在計算機安裝一個解釋器,並由解釋器來解釋執行。
4、解釋執行和編譯執行都有各自的優缺點:
解釋執行啟動速度快,但是執行時速度慢,
而編譯執行啟動速度慢,但是執行速度快。
為了充分地利用解釋執行和編譯執行的優點,規避其缺點:
V8 采用了一種權衡策略,在啟動過程中采用了解釋執行的策略,但是如果某段代碼的執行頻率超過一個值,那么 V8 就會采用優化編譯器將其編譯成執行效率更加高效的機器代碼。
5、總結:
V8 執行一段 JavaScript 代碼所經歷的主要流程包括:
(1)初始化基礎環境;
(2)解析源碼生成 AST 和作用域;
(3)依據 AST 和作用域生成字節碼;
(4)解釋執行字節碼;
(5)監聽熱點代碼;
(6)優化熱點代碼為二進制的機器代碼;
(7)反優化生成的二進制機器代碼。
作者:獨釣寒江雪
原文鏈接:https://segmentfault.com/a/1190000037435824
