新聞中心
V8 是由 Google 開發(fā)的開源 JavaScript 引擎,也被稱為虛擬機(jī),模擬實(shí)際計(jì)算機(jī)各種功能來實(shí)現(xiàn)代碼的編譯和執(zhí)行。

記得那年花下,深夜,初識謝娘時(shí)
為什么需要 JavaScript 引擎
我們寫的 JavaScript 代碼直接交給瀏覽器或者 Node 執(zhí)行時(shí),底層的 CPU 是不認(rèn)識的,也沒法執(zhí)行。CPU 只認(rèn)識自己的指令集,指令集對應(yīng)的是匯編代碼。寫匯編代碼是一件很痛苦的事情。并且不同類型的 CPU 的指令集是不一樣的,那就意味著需要給每一種 CPU 重寫匯編代碼。
JavaScirpt 引擎可以將 JS 代碼編譯為不同 CPU(Intel, ARM 以及 MIPS 等)對應(yīng)的匯編代碼,這樣我們就不需要去翻閱每個(gè) CPU 的指令集手冊來編寫匯編代碼了。當(dāng)然,JavaScript 引擎的工作也不只是編譯代碼,它還要負(fù)責(zé)執(zhí)行代碼、分配內(nèi)存以及垃圾回收。
- 1000100111011000 #機(jī)器指令
- mov ax,bx #匯編指令
資料拓展: 匯編語言入門教程【阮一峰】 | 理解 V8 的字節(jié)碼「譯」
https://zhuanlan.zhihu.com/p/28590489
熱門 JavaScript 引擎
- V8 (Google),用 C++編寫,開放源代碼,由 Google 丹麥開發(fā),是 Google Chrome 的一部分,也用于 Node.js。
- JavaScriptCore (Apple),開放源代碼,用于 webkit 型瀏覽器,如 Safari ,2008 年實(shí)現(xiàn)了編譯器和字節(jié)碼解釋器,升級為了 SquirrelFish。蘋果內(nèi)部代號為“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
- Rhino,由 Mozilla 基金會管理,開放源代碼,完全以 Java 編寫,用于 HTMLUnit
- SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,現(xiàn)時(shí)用于 Mozilla Firefox。
- Chakra (JScript 引擎),用于 Internet Explorer。
- Chakra (JavaScript 引擎),用于 Microsoft Edge。
- KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波頓開發(fā),用于 KDE 項(xiàng)目的 Konqueror 網(wǎng)頁瀏覽器中。
- JerryScript — 三星推出的適用于嵌入式設(shè)備的小型 JavaScript 引擎。
- 其他:Nashorn、QuickJS 、 Hermes
V8
Google V8 引擎是用 C ++編寫的開源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以運(yùn)行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 處理器的 Linux 系統(tǒng)上。V8 最早被開發(fā)用以嵌入到 Google 的開源瀏覽器 Chrome 中,第一個(gè)版本隨著第一版Chrome于 2008 年 9 月 2 日發(fā)布。但是 V8 是一個(gè)可以獨(dú)立運(yùn)行的模塊,完全可以嵌入到任何 C ++應(yīng)用程序中。著名的 Node.js( 一個(gè)異步的服務(wù)器框架,可以在服務(wù)端使用 JavaScript 寫出高效的網(wǎng)絡(luò)服務(wù)器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。??
和其他 JavaScript 引擎一樣,V8 會編譯 / 執(zhí)行 JavaScript 代碼,管理內(nèi)存,負(fù)責(zé)垃圾回收,與宿主語言的交互等。通過暴露宿主對象 ( 變量,函數(shù)等 ) 到 JavaScript,JavaScript 可以訪問宿主環(huán)境中的對象,并在腳本中完成對宿主對象的操作。
與君初相識,猶如故人歸
什么是 D8
d8 是一個(gè)非常有用的調(diào)試工具,你可以把它看成是 debug for V8 的縮寫。我們可以使用 d8 來查看 V8 在執(zhí)行 JavaScript 過程中的各種中間數(shù)據(jù),比如作用域、AST、字節(jié)碼、優(yōu)化的二進(jìn)制代碼、垃圾回收的狀態(tài),還可以使用 d8 提供的私有 API 查看一些內(nèi)部信息。
安裝 D8
- 方法一:自行下載編譯
- v8 google 下載及編譯使用
- 官方文檔:Using d8
- 方法二:使用編譯好的 d8 工具
- mac 平臺:
https://storage.googleapis.com/chromium-v8/official/canary/v8-mac64-dbg-8.4.109.zip
- linux32 平臺:
https://storage.googleapis.com/chromium-v8/official/canary/v8-linux32-dbg-8.4.109.zip
- linux64 平臺:
https://storage.googleapis.com/chromium-v8/official/canary/v8-linux64-dbg-8.4.109.zip
- win32 平臺:
https://storage.googleapis.com/chromium-v8/official/canary/v8-win32-dbg-8.4.109.zip
- win64 平臺:
https://storage.googleapis.com/chromium-v8/official/canary/v8-win64-dbg-8.4.109.zip
- // 解壓文件,點(diǎn)擊d8打開(mac安全策略限制的話,按住control,再點(diǎn)擊,彈出菜單中選擇打開)
- V8 version 8.4.109
- d8> 1 + 2
- 3
- d8> 2 + '4'
- "24"
- d8> console.log(23)
- 23
- undefined
- d8> var a = 1
- undefined
- d8> a + 2
- 3
- d8> this
- [object global]
- d8>
本文后續(xù)用于 demo 演示時(shí)的文件目錄結(jié)構(gòu):
- V8:
- # d8可執(zhí)行文件
- d8
- icudtl.dat
- libc++.dylib
- libchrome_zlib.dylib
- libicui18n.dylib
- libicuuc.dylib
- libv8.dylib
- libv8_debug_helper.dylib
- libv8_for_testing.dylib
- libv8_libbase.dylib
- libv8_libplatform.dylib
- obj
- snapshot_blob.bin
- v8_build_config.json
- # 新建的js示例文件
- test.js
- 方法三:mac
- # 如果已有HomeBrew,忽略第一條命令
- ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- brew install v8
- 方法四:使用 node 代替,比如可以用node --print-bytecode ./test.js,打印出 Ignition(解釋器)生成的 Bytecode(字節(jié)碼)。
都有哪些 d8 命令可供使用?
- 查看 d8 命令
- # 如果不想使用./d8這種方式進(jìn)行調(diào)試,可將d8加入環(huán)境變量,之后就可以直接`d8 --help`了
- ./d8 --help
- 過濾特定的命令,如:
- # 如果是 Windows 系統(tǒng),可能缺少 grep 程序,請自行下載安裝并添加環(huán)境變量
- ./d8 --help |grep print
- print-bytecode 查看生成的字節(jié)碼
- print-opt-code 查看優(yōu)化后的代碼
- print-ast 查看中間生成的 AST
- print-scopes 查看中間生成的作用域
- trace-gc 查看這段代碼的內(nèi)存回收狀態(tài)
- trace-opt 查看哪些代碼被優(yōu)化了
- trace-deopt 查看哪些代碼被反優(yōu)化了
- turbofan-stats 打印優(yōu)化編譯器的一些統(tǒng)計(jì)數(shù)據(jù)
使用 d8 進(jìn)行調(diào)試
- // test.js
- function sum(a) {
- var b = 6;
- return a + 6;
- }
- console.log(sum(3));
- # d8 后面跟上文件名和要執(zhí)行的命令,如執(zhí)行下面這行命令,就會打印出 test.js 文件所生成的字節(jié)碼。
- ./d8 ./test.js --print-bytecode
- # 執(zhí)行以下命令,輸出9
- ./d8 ./test.js
內(nèi)部方法
你還可以使用 V8 所提供的一些內(nèi)部方法,只需要在啟動(dòng) V8 時(shí)傳入 --allow-natives-syntax 命令,你就可以在 test.js 中使用諸如HasFastProperties(檢查一個(gè)對象是否擁有快屬性)的內(nèi)部方法(索引屬性、常規(guī)屬性、快屬性等下文會介紹)。
- function Foo(property_num, element_num) {
- //添加可索引屬性
- for (let i = 0; i < element_num; i++) {
- this[i] = `element${i}`;
- }
- //添加常規(guī)屬性
- for (let i = 0; i < property_num; i++) {
- let ppt = `property${i}`;
- this[ppt] = ppt;
- }
- }
- var bar = new Foo(10, 10);
- // 檢查一個(gè)對象是否擁有快屬性
- console.log(%HasFastProperties(bar));
- delete bar.property2;
- console.log(%HasFastProperties(bar));
- ./d8 --allow-natives-syntax ./test.js
- # 依次打印:true false
心似雙絲網(wǎng),中有千千結(jié)
V8 引擎的內(nèi)部結(jié)構(gòu)
V8 是一個(gè)非常復(fù)雜的項(xiàng)目,有超過 100 萬行 C++代碼。它由許多子模塊構(gòu)成,其中這 4 個(gè)模塊是最重要的:
- Parser:負(fù)責(zé)將 JavaScript 源碼轉(zhuǎn)換為 Abstract Syntax Tree (AST)
- Ignition:interpreter,即解釋器,負(fù)責(zé)將 AST 轉(zhuǎn)換為 Bytecode,解釋執(zhí)行 Bytecode;同時(shí)收集 TurboFan 優(yōu)化編譯所需的信息,比如函數(shù)參數(shù)的類型;解釋器執(zhí)行時(shí)主要有四個(gè)模塊,內(nèi)存中的字節(jié)碼、寄存器、棧、堆。
通常有兩種類型的解釋器,基于棧 (Stack-based)和基于寄存器 (Register-based),基于棧的解釋器使用棧來保存函數(shù)參數(shù)、中間運(yùn)算結(jié)果、變量等;基于寄存器的虛擬機(jī)則支持寄存器的指令操作,使用寄存器來保存參數(shù)、中間計(jì)算結(jié)果。通常,基于棧的虛擬機(jī)也定義了少量的寄存器,基于寄存器的虛擬機(jī)也有堆棧,其區(qū)別體現(xiàn)在它們提供的指令集體系。大多數(shù)解釋器都是基于棧的,比如 Java 虛擬機(jī),.Net 虛擬機(jī),還有早期的 V8 虛擬機(jī)?;诙褩5奶摂M機(jī)在處理函數(shù)調(diào)用、解決遞歸問題和切換上下文時(shí)簡單明快。而現(xiàn)在的 V8 虛擬機(jī)則采用了基于寄存器的設(shè)計(jì),它將一些中間數(shù)據(jù)保存到寄存器中。
基于寄存器的解釋器架構(gòu):
- TurboFan:compiler,即編譯器,利用 Ignitio 所收集的類型信息,將 Bytecode 轉(zhuǎn)換為優(yōu)化的匯編代碼;
- Orinoco:garbage collector,垃圾回收模塊,負(fù)責(zé)將程序不再需要的內(nèi)存空間回收。
其中,Parser,Ignition 以及 TurboFan 可以將 JS 源碼編譯為匯編代碼,其流程圖如下:
??
簡單地說,Parser 將 JS 源碼轉(zhuǎn)換為 AST,然后 Ignition 將 AST 轉(zhuǎn)換為 Bytecode,最后 TurboFan 將 Bytecode 轉(zhuǎn)換為經(jīng)過優(yōu)化的 Machine Code(實(shí)際上是匯編代碼)。
- 如果函數(shù)沒有被調(diào)用,則 V8 不會去編譯它。
- 如果函數(shù)只被調(diào)用 1 次,則 Ignition 將其編譯 Bytecode 就直接解釋執(zhí)行了。TurboFan 不會進(jìn)行優(yōu)化編譯,因?yàn)樗枰?Ignition 收集函數(shù)執(zhí)行時(shí)的類型信息。這就要求函數(shù)至少需要執(zhí)行 1 次,TurboFan 才有可能進(jìn)行優(yōu)化編譯。
- 如果函數(shù)被調(diào)用多次,則它有可能會被識別為熱點(diǎn)函數(shù),且 Ignition 收集的類型信息證明可以進(jìn)行優(yōu)化編譯的話,這時(shí) TurboFan 則會將 Bytecode 編譯為 Optimized Machine Code(已優(yōu)化的機(jī)器碼),以提高代碼的執(zhí)行性能。??
圖片中的紅色虛線是逆向的,也就是說Optimized Machine Code 會被還原為 Bytecode,這個(gè)過程叫做 Deoptimization。這是因?yàn)?Ignition 收集的信息可能是錯(cuò)誤的,比如 add 函數(shù)的參數(shù)之前是整數(shù),后來又變成了字符串。生成的 Optimized Machine Code 已經(jīng)假定 add 函數(shù)的參數(shù)是整數(shù),那當(dāng)然是錯(cuò)誤的,于是需要進(jìn)行 Deoptimization。
- function add(x, y) {
- return x + y;
- }
- add(3, 5);
- add('3', '5');
在運(yùn)行 C、C++以及 Java 等程序之前,需要進(jìn)行編譯,不能直接執(zhí)行源碼;但對于 JavaScript 來說,我們可以直接執(zhí)行源碼(比如:node test.js),它是在運(yùn)行的時(shí)候先編譯再執(zhí)行,這種方式被稱為即時(shí)編譯(Just-in-time compilation),簡稱為 JIT。因此,V8 也屬于 JIT 編譯器。
V8 是怎么執(zhí)行一段 JavaScript 代碼的
- 在 V8 出現(xiàn)之前,所有的 JavaScript 虛擬機(jī)所采用的都是解釋執(zhí)行的方式,這是 JavaScript 執(zhí)行速度過慢的一個(gè)主要原因。而 V8 率先引入了即時(shí)編譯(JIT)的雙輪驅(qū)動(dòng)的設(shè)計(jì)(混合使用編譯器和解釋器的技術(shù)),這是一種權(quán)衡策略,混合編譯執(zhí)行和解釋執(zhí)行這兩種手段,給 JavaScript 的執(zhí)行速度帶來了極大的提升。V8 出現(xiàn)之后,各大廠商也都在自己的 JavaScript 虛擬機(jī)中引入了 JIT 機(jī)制,所以目前市面上 JavaScript 虛擬機(jī)都有著類似的架構(gòu)。另外,V8 也是早于其他虛擬機(jī)引入了惰性編譯、內(nèi)聯(lián)緩存、隱藏類等機(jī)制,進(jìn)一步優(yōu)化了 JavaScript 代碼的編譯執(zhí)行效率。
- V8 執(zhí)行一段 JavaScript 的流程圖:
- V8 本質(zhì)上是一個(gè)虛擬機(jī),因?yàn)橛?jì)算機(jī)只能識別二進(jìn)制指令,所以要讓計(jì)算機(jī)執(zhí)行一段高級語言通常有兩種手段:
- 第一種是將高級代碼轉(zhuǎn)換為二進(jìn)制代碼,再讓計(jì)算機(jī)去執(zhí)行;
- 另外一種方式是在計(jì)算機(jī)安裝一個(gè)解釋器,并由解釋器來解釋執(zhí)行。
- 解釋執(zhí)行和編譯執(zhí)行都有各自的優(yōu)缺點(diǎn),解釋執(zhí)行啟動(dòng)速度快,但是執(zhí)行時(shí)速度慢,而編譯執(zhí)行啟動(dòng)速度慢,但是執(zhí)行速度快。為了充分地利用解釋執(zhí)行和編譯執(zhí)行的優(yōu)點(diǎn),規(guī)避其缺點(diǎn),V8 采用了一種權(quán)衡策略,在啟動(dòng)過程中采用了解釋執(zhí)行的策略,但是如果某段代碼的執(zhí)行頻率超過一個(gè)值,那么 V8 就會采用優(yōu)化編譯器將其編譯成執(zhí)行效率更加高效的機(jī)器代碼。
- 總結(jié):
V8 執(zhí)行一段 JavaScript 代碼所經(jīng)歷的主要流程包括:
- 初始化基礎(chǔ)環(huán)境;
- 解析源碼生成 AST 和作用域;
- 依據(jù) AST 和作用域生成字節(jié)碼;
- 解釋執(zhí)行字節(jié)碼;
- 監(jiān)聽熱點(diǎn)代碼;
- 優(yōu)化熱點(diǎn)代碼為二進(jìn)制的機(jī)器代碼;
- 反優(yōu)化生成的二進(jìn)制機(jī)器代碼。
一等公民與閉包
一等公民的定義
- 在編程語言中,一等公民可以作為函數(shù)參數(shù),可以作為函數(shù)返回值,也可以賦值給變量。
- 如果某個(gè)編程語言的函數(shù),可以和這個(gè)語言的數(shù)據(jù)類型做一樣的事情,我們就把這個(gè)語言中的函數(shù)稱為一等公民。例如,字符串在幾乎所有編程語言中都是一等公民,字符串可以做為函數(shù)參數(shù),字符串可以作為函數(shù)返回值,字符串也可以賦值給變量。對于各種編程語言來說,函數(shù)就不一定是一等公民了,比如 Java 8 之前的版本。
- 對于 JavaScript 來說,函數(shù)可以賦值給變量,也可以作為函數(shù)參數(shù),還可以作為函數(shù)返回值,因此 JavaScript 中函數(shù)是一等公民。
動(dòng)態(tài)作用域與靜態(tài)作用域
- 如果一門語言的作用域是靜態(tài)作用域,那么符號之間的引用關(guān)系能夠根據(jù)程序代碼在編譯時(shí)就確定清楚,在運(yùn)行時(shí)不會變。某個(gè)函數(shù)是在哪聲明的,就具有它所在位置的作用域。它能夠訪問哪些變量,那么就跟這些變量綁定了,在運(yùn)行時(shí)就一直能訪問這些變量。即靜態(tài)作用域可以由程序代碼決定,在編譯時(shí)就能完全確定。大多數(shù)語言都是靜態(tài)作用域的。
- 動(dòng)態(tài)作用域(Dynamic Scope)。也就是說,變量引用跟變量聲明不是在編譯時(shí)就綁定死了的。在運(yùn)行時(shí),它是在運(yùn)行環(huán)境中動(dòng)態(tài)地找一個(gè)相同名稱的變量。在 macOS 或 Linux 中用的 bash 腳本語言,就是動(dòng)態(tài)作用域的。
閉包的三個(gè)基礎(chǔ)特性
- JavaScript 語言允許在函數(shù)內(nèi)部定義新的函數(shù)
- 可以在內(nèi)部函數(shù)中訪問父函數(shù)中定義的變量
- 因?yàn)?JavaScript 中的函數(shù)是一等公民,所以函數(shù)可以作為另外一個(gè)函數(shù)的返回值
- // 閉包(靜態(tài)作用域,一等公民,調(diào)用棧的矛盾體)
- function foo() {
- var d = 20;
- return function inner(a, b) {
- const c = a + b + d;
- return c;
- };
- }
- const f = foo();
關(guān)于閉包,可參考我以前的一篇文章,在此不再贅述,在此主要談下閉包給 Chrome V8 帶來的問題及其解決策略。
惰性解析??
所謂惰性解析是指解析器在解析的過程中,如果遇到函數(shù)聲明,那么會跳過函數(shù)內(nèi)部的代碼,并不會為其生成 AST 和字節(jié)碼,而僅僅生成頂層代碼的 AST 和字節(jié)碼。
- 在編譯 JavaScript 代碼的過程中,V8 并不會一次性將所有的 JavaScript 解析為中間代碼,這主要是基于以下兩點(diǎn):
- 首先,如果一次解析和編譯所有的 JavaScript 代碼,過多的代碼會增加編譯時(shí)間,這會嚴(yán)重影響到首次執(zhí)行 JavaScript 代碼的速度,讓用戶感覺到卡頓。因?yàn)橛袝r(shí)候一個(gè)頁面的 JavaScript 代碼很大,如果要將所有的代碼一次性解析編譯完成,那么會大大增加用戶的等待時(shí)間;
- 其次,解析完成的字節(jié)碼和編譯之后的機(jī)器代碼都會存放在內(nèi)存中,如果一次性解析和編譯所有 JavaScript 代碼,那么這些中間代碼和機(jī)器代碼將會一直占用內(nèi)存。
- 基于以上的原因,所有主流的 JavaScript 虛擬機(jī)都實(shí)現(xiàn)了惰性解析。
- 閉包給惰性解析帶來的問題:上文的 d 不能隨著 foo 函數(shù)的執(zhí)行上下文被銷毀掉。
預(yù)解析器
V8 引入預(yù)解析器,比如當(dāng)解析頂層代碼的時(shí)候,遇到了一個(gè)函數(shù),那么預(yù)解析器并不會直接跳過該函數(shù),而是對該函數(shù)做一次快速的預(yù)解析。
- 判斷當(dāng)前函數(shù)是不是存在一些語法上的錯(cuò)誤,發(fā)現(xiàn)了語法錯(cuò)誤,那么就會向 V8 拋出語法錯(cuò)誤;
- 檢查函數(shù)內(nèi)部是否引用了外部變量,如果引用了外部的變量,預(yù)解析器會將棧中的變量復(fù)制到堆中,在下次執(zhí)行到該函數(shù)的時(shí)候,直接使用堆中的引用,這樣就解決了閉包所帶來的問題。
V8 內(nèi)部是如何存儲對象的:快屬性和慢屬性
下面的代碼會輸出什么:
- // test.js
- function Foo() {
- this[200] = 'test-200';
- this[1] = 'test-1';
- this[100] = 'test-100';
- this['B'] = 'bar-B';
- this[50] = 'test-50';
- this[9] = 'test-9';
- this[8] = 'test-8';
- this[3] = 'test-3';
- this[5] = 'test-5';
- this['D'] = 'bar-D';
- this['C'] = 'bar-C';
- }
- var bar = new Foo();
- for (key in bar) {
- console.log(`index:${key} value:${bar[key]}`);
- }
- //輸出:
- // index:1 value:test-1
- // index:3 value:test-3
- // index:5 value:test-5
- // index:8 value:test-8
- // index:9 value:test-9
- // index:50 value:test-50
- // index:100 value:test-100
- // index:200 value:test-200
- // index:B value:bar-B
- // index:D value:bar-D
- // index:C value:bar-C
在ECMAScript 規(guī)范中定義了數(shù)字屬性應(yīng)該按照索引值大小升序排列,字符串屬性根據(jù)創(chuàng)建時(shí)的順序升序排列。在這里我們把對象中的數(shù)字屬性稱為排序?qū)傩裕?V8 中被稱為 elements,字符串屬性就被稱為常規(guī)屬性,在 V8 中被稱為 properties。在 V8 內(nèi)部,為了有效地提升存儲和訪問這兩種屬性的性能,分別使用了兩個(gè)線性數(shù)據(jù)結(jié)構(gòu)來分別保存排序?qū)傩院统R?guī)屬性。同時(shí) v8 將部分常規(guī)屬性直接存儲到對象本身,我們把這稱為對象內(nèi)屬性 (in-object properties),不過對象內(nèi)屬性的數(shù)量是固定的,默認(rèn)是 10 個(gè)。
- function Foo(property_num, element_num) {
- //添加可索引屬性
- for (let i = 0; i < element_num; i++) {
- this[i] = `element${i}`;
- }
- //添加常規(guī)屬性
- for (let i = 0; i < property_num; i++) {
- let ppt = `property${i}`;
- this[ppt] = ppt;
- }
- }
- var bar = new Foo(10, 10);
可以通過 Chrome 開發(fā)者工具的 Memory 標(biāo)簽,捕獲查看當(dāng)前的內(nèi)存快照。通過增大第一個(gè)參數(shù)來查看存儲變化。
我們將保存在線性數(shù)據(jù)結(jié)構(gòu)中的屬性稱之為“快屬性”,因?yàn)榫€性數(shù)據(jù)結(jié)構(gòu)中只需要通過索引即可以訪問到屬性,雖然訪問線性結(jié)構(gòu)的速度快,但是如果從線性結(jié)構(gòu)中添加或者刪除大量的屬性時(shí),則執(zhí)行效率會非常低,這主要因?yàn)闀a(chǎn)生大量時(shí)間和內(nèi)存開銷。因此,如果一個(gè)對象的屬性過多時(shí),V8 就會采取另外一種存儲策略,那就是“慢屬性”策略,但慢屬性的對象內(nèi)部會有獨(dú)立的非線性數(shù)據(jù)結(jié)構(gòu) (字典) 作為屬性存儲容器。所有的屬性元信息不再是線性存儲的,而是直接保存在屬性字典中。
v8 屬性存儲:
總結(jié):??
因?yàn)?JavaScript 中的對象是由一組組屬性和值組成的,所以最簡單的方式是使用一個(gè)字典來保存屬性和值,但是由于字典是非線性結(jié)構(gòu),所以如果使用字典,讀取效率會大大降低。為了提升查找效率,V8 在對象中添加了兩個(gè)隱藏屬性,排序?qū)傩院统R?guī)屬性,element 屬性指向了 elements 對象,在 elements 對象中,會按照順序存放排序?qū)傩?。properties 屬性則指向了 properties 對象,在 properties 對象中,會按照創(chuàng)建時(shí)的順序保存常規(guī)屬性。??
通過引入這兩個(gè)屬性,加速了 V8 查找屬性的速度,為了更加進(jìn)一步提升查找效率,V8 還實(shí)現(xiàn)了內(nèi)置內(nèi)屬性的策略,當(dāng)常規(guī)屬性少于一定數(shù)量時(shí),V8 就會將這些常規(guī)屬性直接寫進(jìn)對象中,這樣又節(jié)省了一個(gè)中間步驟。??
但是如果對象中的屬性過多時(shí),或者存在反復(fù)添加或者刪除屬性的操作,那么 V8 就會將線性的存儲模式降級為非線性的字典存儲模式,這樣雖然降低了查找速度,但是卻提升了修改對象的屬性的速度。
堆空間和棧空間
??臻g
- 現(xiàn)代語言都是基于函數(shù)的,每個(gè)函數(shù)在執(zhí)行過程中,都有自己的生命周期和作用域,當(dāng)函數(shù)執(zhí)行結(jié)束時(shí),其作用域也會被銷毀,因此,我們會使用棧這種數(shù)據(jù)結(jié)構(gòu)來管理函數(shù)的調(diào)用過程,我們也把管理函數(shù)調(diào)用過程的棧結(jié)構(gòu)稱之為調(diào)用棧。
- 棧空間主要是用來管理 JavaScript 函數(shù)調(diào)用的,棧是內(nèi)存中連續(xù)的一塊空間,同時(shí)棧結(jié)構(gòu)是“先進(jìn)后出”的策略。在函數(shù)調(diào)用過程中,涉及到上下文相關(guān)的內(nèi)容都會存放在棧上,比如原生類型、引用到的對象的地址、函數(shù)的執(zhí)行狀態(tài)、this 值等都會存在在棧上。當(dāng)一個(gè)函數(shù)執(zhí)行結(jié)束,那么該函數(shù)的執(zhí)行上下文便會被銷毀掉。
- ??臻g的最大的特點(diǎn)是空間連續(xù),所以在棧中每個(gè)元素的地址都是固定的,因此??臻g的查找效率非常高,但是通常在內(nèi)存中,很難分配到一塊很大的連續(xù)空間,因此,V8 對??臻g的大小做了限制,如果函數(shù)調(diào)用層過深,那么 V8 就有可能拋出棧溢出的錯(cuò)誤。
- 棧的優(yōu)勢和缺點(diǎn):
- 棧的結(jié)構(gòu)非常適合函數(shù)調(diào)用過程。
- 在棧上分配資源和銷毀資源的速度非常快,這主要?dú)w結(jié)于??臻g是連續(xù)的,分配空間和銷毀空間只需要移動(dòng)下指針就可以了。
- 雖然操作速度非???,但是棧也是有缺點(diǎn)的,其中最大的缺點(diǎn)也是它的優(yōu)點(diǎn)所造成的,那就是棧是連續(xù)的,所以要想在內(nèi)存中分配一塊連續(xù)的大空間是非常難的,因此??臻g是有限的。
- // 棧溢出
- function factorial(n) {
- if (n === 1) {
- return 1;
- }
- return n * factorial(n - 1);
- }
- console.log(factorial(50000));
堆空間
- 堆空間是一種樹形的存儲結(jié)構(gòu),用來存儲對象類型的離散的數(shù)據(jù),JavaScript 中除了原生類型的數(shù)據(jù),其他的都是對象類型,諸如函數(shù)、數(shù)組,在瀏覽器中還有 window 對象、document 對象等,這些都是存在堆空間的。
- 宿主在啟動(dòng) V8 的過程中,會同時(shí)創(chuàng)建堆空間和棧空間,再繼續(xù)往下執(zhí)行,產(chǎn)生的新數(shù)據(jù)都會存放在這兩個(gè)空間中。
繼承
繼承就是一個(gè)對象可以訪問另外一個(gè)對象中的屬性和方法,在 JavaScript 中,我們通過原型和原型鏈的方式來實(shí)現(xiàn)了繼承特性。
JavaScript 的每個(gè)對象都包含了一個(gè)隱藏屬性 __proto__ ,我們就把該隱藏屬性 __proto__ 稱之為該對象的原型 (prototype),__proto__ 指向了內(nèi)存中的另外一個(gè)對象,我們就把 __proto__ 指向的對象稱為該對象的原型對象,那么該對象就可以直接訪問其原型對象的方法或者屬性。??
JavaScript 中的繼承非常簡潔,就是每個(gè)對象都有一個(gè)原型屬性,該屬性指向了原型對象,查找屬性的時(shí)候,JavaScript 虛擬機(jī)會沿著原型一層一層向上查找,直至找到正確的屬性。
隱藏屬性__proto__
- var animal = {
- type: 'Default',
- color: 'Default',
- getInfo: function () {
- return `Type is: ${this.type},color is ${this.color}.`;
- },
- };
- var dog = {
- type: 'Dog',
- color: 'Black',
- };
利用__proto__實(shí)現(xiàn)繼承:
- dog.__proto__ = animal;
- dog.getInfo();
通常隱藏屬性是不能使用 JavaScript 來直接與之交互的。雖然現(xiàn)代瀏覽器都開了一個(gè)口子,讓 JavaScript 可以訪問隱藏屬性 __proto__,但是在實(shí)際項(xiàng)目中,我們不應(yīng)該直接通過 __proto__ 來訪問或者修改該屬性,其主要原因有兩個(gè):
- 首先,這是隱藏屬性,并不是標(biāo)準(zhǔn)定義的;
- 其次,使用該屬性會造成嚴(yán)重的性能問題。因?yàn)?JavaScript 通過隱藏類優(yōu)化了很多原有的對象結(jié)構(gòu),所以通過直接修改__proto__會直接破壞現(xiàn)有已經(jīng)優(yōu)化的結(jié)構(gòu),觸發(fā) V8 重構(gòu)該對象的隱藏類!
構(gòu)造函數(shù)是怎么創(chuàng)建對象的??
在 JavaScript 中,使用 new 加上構(gòu)造函數(shù)的這種組合來創(chuàng)建對象和實(shí)現(xiàn)對象的繼承。不過使用這種方式隱含的語義過于隱晦。其實(shí)是 JavaScript 為了吸引 Java 程序員、在語法層面去蹭 Java 熱點(diǎn),所以就被硬生生地強(qiáng)制加入了非常不協(xié)調(diào)的關(guān)鍵字 new。
- function DogFactory(type, color) {
- this.type = type;
- this.color = color;
- }
- var dog = new DogFactory('Dog', 'Black');
其實(shí)當(dāng) V8 執(zhí)行上面這段代碼時(shí),V8 在背后悄悄地做了以下幾件事情:
- var dog = {};
- dog.__proto__ = DogFactory.prototype;
- DogFactory.call(dog, 'Dog', 'Black');
機(jī)器碼、字節(jié)碼
V8 為什么要引入字節(jié)碼
- 早期的 V8 為了提升代碼的執(zhí)行速度,直接將 JavaScript 源代碼編譯成了沒有優(yōu)化的二進(jìn)制機(jī)器代碼,如果某一段二進(jìn)制代碼執(zhí)行頻率過高,那么 V8 會將其標(biāo)記為熱點(diǎn)代碼,熱點(diǎn)代碼會被優(yōu)化編譯器優(yōu)化,優(yōu)化后的機(jī)器代碼執(zhí)行效率更高。
- 隨著移動(dòng)設(shè)備的普及,V8 團(tuán)隊(duì)逐漸發(fā)現(xiàn)將 JavaScript 源碼直接編譯成二進(jìn)制代碼存在兩個(gè)致命的問題:
- 時(shí)間問題:編譯時(shí)間過久,影響代碼啟動(dòng)速度;
- 空間問題:緩存編譯后的二進(jìn)制代碼占用更多的內(nèi)存。
- 這兩個(gè)問題無疑會阻礙 V8 在移動(dòng)設(shè)備上的普及,于是 V8 團(tuán)隊(duì)大規(guī)模重構(gòu)代碼,引入了中間的字節(jié)碼。字節(jié)碼的優(yōu)勢有如下三點(diǎn):
- 解決啟動(dòng)問題:生成字節(jié)碼的時(shí)間很短;
- 解決空間問題:字節(jié)碼雖然占用的空間比原始的 JavaScript 多,但是相較于機(jī)器代碼,字節(jié)碼還是小了太多,緩存字節(jié)碼會大大降低內(nèi)存的使用。
- 代碼架構(gòu)清晰:采用字節(jié)碼,可以簡化程序的復(fù)雜度,使得 V8 移植到不同的 CPU 架構(gòu)平臺更加容易。
- Bytecode 某種程度上就是匯編語言,只是它沒有對應(yīng)特定的 CPU,或者說它對應(yīng)的是虛擬的 CPU。這樣的話,生成 Bytecode 時(shí)簡單很多,無需為不同的 CPU 生產(chǎn)不同的代碼。要知道,V8 支持 9 種不同的 CPU,引入一個(gè)中間層 Bytecode,可以簡化 V8 的編譯流程,提高可擴(kuò)展性。
- 如果我們在不同硬件上去生成 Bytecode,會發(fā)現(xiàn)生成代碼的指令是一樣的。
如何查看字節(jié)碼
- // test.js
- function add(x, y) {
- var z = x + y;
- return z;
- }
- console.log(add(1, 2));
運(yùn)行./d8 ./test.js --print-bytecode:
- [generated bytecode for function: add (0x01000824fe59
)] - Parameter count 3 #三個(gè)參數(shù),包括了顯式地傳入的 x 和 y,還有一個(gè)隱式地傳入的 this
- Register count 1
- Frame size 8
- 0x10008250026 @ 0 : 25 02 Ldar a1 #將a1寄存器中的值加載到累加器中,LoaD Accumulator from Register
- 0x10008250028 @ 2 : 34 03 00 Add a0, [0]
- 0x1000825002b @ 5 : 26 fb Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中
- 0x1000825002d @ 7 : aa Return #結(jié)束當(dāng)前函數(shù)的執(zhí)行,并將控制權(quán)傳回給調(diào)用方
- Constant pool (size = 0)
- Handler Table (size = 0)
- Source Position Table (size = 0)
- 3
常用字節(jié)碼指令:
- Ldar:表示將寄存器中的值加載到累加器中,你可以把它理解為 LoaD Accumulator from Register,就是把某個(gè)寄存器中的值,加載到累加器中。
- Star:表示 Store Accumulator Register, 你可以把它理解為 Store Accumulator to Register,就是把累加器中的值保存到某個(gè)寄存器中
- Add:Add a0, [0]是從 a0 寄存器加載值并將其與累加器中的值相加,然后將結(jié)果再次放入累加器。
add a0 后面的[0]稱之為 feedback vector slot,又叫反饋向量槽,它是一個(gè)數(shù)組,解釋器將解釋執(zhí)行過程中的一些數(shù)據(jù)類型的分析信息都保存在這個(gè)反饋向量槽中了,目的是為了給 TurboFan 優(yōu)化編譯器提供優(yōu)化信息,很多字節(jié)碼都會為反饋向量槽提供運(yùn)行時(shí)信息。
- LdaSmi:將小整數(shù)(Smi)加載到累加器寄存器中
- Return:結(jié)束當(dāng)前函數(shù)的執(zhí)行,并將控制權(quán)傳回給調(diào)用方。返回的值是累加器中的值。
隱藏類和內(nèi)聯(lián)緩存
JavaScript 是一門動(dòng)態(tài)語言,其執(zhí)行效率要低于靜態(tài)語言,V8 為了提升 JavaScript 的執(zhí)行速度,借鑒了很多靜態(tài)語言的特性,比如實(shí)現(xiàn)了 JIT 機(jī)制,為了提升對象的屬性訪問速度而引入了隱藏類,為了加速運(yùn)算而引入了內(nèi)聯(lián)緩存。
為什么靜態(tài)語言的效率更高???
靜態(tài)語言中,如 C++ 在聲明一個(gè)對象之前需要定義該對象的結(jié)構(gòu),代碼在執(zhí)行之前需要先被編譯,編譯的時(shí)候,每個(gè)對象的形狀都是固定的,也就是說,在代碼的執(zhí)行過程中是無法被改變的。可以直接通過偏移量查詢來查詢對象的屬性值,這也就是靜態(tài)語言的執(zhí)行效率高的一個(gè)原因。??
JavaScript 在運(yùn)行時(shí),對象的屬性是可以被修改的,所以當(dāng) V8 使用了一個(gè)對象時(shí),比如使用了 obj.x 的時(shí)候,它并不知道該對象中是否有 x,也不知道 x 相對于對象的偏移量是多少,也就是說 V8 并不知道該對象的具體的形狀。那么,當(dāng)在 JavaScript 中要查詢對象 obj 中的 x 屬性時(shí),V8 會按照具體的規(guī)則一步一步來查詢,這個(gè)過程非常的慢且耗時(shí)。
將靜態(tài)的特性引入到 V8
- V8 采用的一個(gè)思路就是將 JavaScript 中的對象靜態(tài)化,也就是 V8 在運(yùn)行 JavaScript 的過程中,會假設(shè) JavaScript 中的對象是靜態(tài)的。
- 具體地講,V8 對每個(gè)對象做如下兩點(diǎn)假設(shè):
- 對象創(chuàng)建好了之后就不會添加新的屬性;
- 對象創(chuàng)建好了之后也不會刪除屬性。
- 符合這兩個(gè)假設(shè)之后,V8 就可以對 JavaScript 中的對象做深度優(yōu)化了。V8 會為每個(gè)對象創(chuàng)建一個(gè)隱藏類,對象的隱藏類中記錄了該對象一些基礎(chǔ)的布局信息,包括以下兩點(diǎn):
- 對象中所包含的所有的屬性;
- 每個(gè)屬性相對于對象的偏移量。
- 有了隱藏類之后,那么當(dāng) V8 訪問某個(gè)對象中的某個(gè)屬性時(shí),就會先去隱藏類中查找該屬性相對于它的對象的偏移量,有了偏移量和屬性類型,V8 就可以直接去內(nèi)存中取出對應(yīng)的屬性值,而不需要經(jīng)歷一系列的查找過程,那么這就大大提升了 V8 查找對象的效率。
- 在 V8 中,把隱藏類又稱為 map,每個(gè)對象都有一個(gè) map 屬性,其值指向內(nèi)存中的隱藏類;
- map 描述了對象的內(nèi)存布局,比如對象都包括了哪些屬性,這些數(shù)據(jù)對應(yīng)于對象的偏移量是多少。
通過 d8 查看隱藏類
- // test.js
- let point1 = { x: 100, y: 200 };
- let point2 = { x: 200, y: 300 };
- let point3 = { x: 100 };
- %DebugPrint(point1);
- %DebugPrint(point2);
- %DebugPrint(point3);
- ./d8 --allow-natives-syntax ./test.js
- # ===============
- DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE]
- # V8 為 point1 對象創(chuàng)建的隱藏類
- - map: 0x1ea308284ce9
- - prototype: 0x1ea308241395
- - elements: 0x1ea3080406e9
[HOLEY_ELEMENTS] - - properties: 0x1ea3080406e9
{ - #x: 100 (const data field 0)
- #y: 200 (const data field 1)
- }
- 0x1ea308284ce9: [Map]
- - type: JS_OBJECT_TYPE
- - instance size: 20
- - inobject properties: 2
- - elements kind: HOLEY_ELEMENTS
- - unused property fields: 0
- - enum length: invalid
- - stable_map
- - back pointer: 0x1ea308284cc1
- - prototype_validity cell: 0x1ea3081c0451
| - - instance descriptors (own) #2: 0x1ea3080c5bf5
- - prototype: 0x1ea308241395
- - constructor: 0x1ea3082413b1
- - dependent code: 0x1ea3080401ed
- - construction counter: 0
- # ===============
- DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE]
- # V8 為 point2 對象創(chuàng)建的隱藏類
- - map: 0x1ea308284ce9
- - prototype: 0x1ea308241395
- - elements: 0x1ea3080406e9
[HOLEY_ELEMENTS] - - properties: 0x1ea3080406e9
{ - #x: 200 (const data field 0)
- #y: 300 (const data field 1)
- }
- 0x1ea308284ce9: [Map]
- - type: JS_OBJECT_TYPE
- - instance size: 20
- - inobject properties: 2
- - elements kind: HOLEY_ELEMENTS
- - unused property fields: 0
- - enum length: invalid
- - stable_map
- - back pointer: 0x1ea308284cc1
- - prototype_validity cell: 0x1ea3081c0451
| - - instance descriptors (own) #2: 0x1ea3080c5bf5
- - prototype: 0x1ea308241395
- - constructor: 0x1ea3082413b1
- - dependent code: 0x1ea3080401ed
- - construction counter: 0
- # ===============
- DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE]
- # V8 為 point3 對象創(chuàng)建的隱藏類
- - map: 0x1ea308284d39
- - prototype: 0x1ea308241395
- - elements: 0x1ea3080406e9
[HOLEY_ELEMENTS] - - properties: 0x1ea3080406e9
{ - #x: 100 (const data field 0)
- }
- 0x1ea308284d39: [Map]
- - type: JS_OBJECT_TYPE
- - instance size: 16
- - inobject properties: 1
- - elements kind: HOLEY_ELEMENTS
- - unused property fields: 0
- - enum length: invalid
- - stable_map
- - back pointer: 0x1ea308284d11
- - prototype_validity cell: 0x1ea3081c0451
| - - instance descriptors (own) #1: 0x1ea3080c5c41
- - prototype: 0x1ea308241395
- - constructor: 0x1ea3082413b1
- - dependent code: 0x1ea3080401ed
- - construction counter: 0
多個(gè)對象共用一個(gè)隱藏類
- 在 V8 中,每個(gè)對象都有一個(gè) map 屬性,該屬性值指向該對象的隱藏類。不過如果兩個(gè)對象的形狀是相同的,V8 就會為其復(fù)用同一個(gè)隱藏類,這樣有兩個(gè)好處:
- 減少隱藏類的創(chuàng)建次數(shù),也間接加速了代碼的執(zhí)行速度;
- 減少了隱藏類的存儲空間。
- 那么,什么情況下兩個(gè)對象的形狀是相同的,要滿足以下兩點(diǎn):
- 相同的屬性名稱;
- 相等的屬性個(gè)數(shù)。
重新構(gòu)建隱藏類
- 給一個(gè)對象添加新的屬性,刪除新的屬性,或者改變某個(gè)屬性的數(shù)據(jù)類型都會改變這個(gè)對象的形狀,那么勢必也就會觸發(fā) V8 為改變形狀后的對象重建新的隱藏類。
- // test.js
- let point = {};
- %DebugPrint(point);
- point.x = 100;
- %DebugPrint(point);
- point.y = 200;
- %DebugPrint(point);
- # ./d8 --allow-natives-syntax ./test.js
- DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- - map: 0x32c7082802d9
- ...
- DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- - map: 0x32c708284cc1
- ...
- DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
- - map: 0x32c708284ce9


咨詢
建站咨詢
