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

記得那年花下,深夜,初識謝娘時
為什么需要 JavaScript 引擎
我們寫的 JavaScript 代碼直接交給瀏覽器或者 Node 執(zhí)行時,底層的 CPU 是不認識的,也沒法執(zhí)行。CPU 只認識自己的指令集,指令集對應(yīng)的是匯編代碼。寫匯編代碼是一件很痛苦的事情。并且不同類型的 CPU 的指令集是不一樣的,那就意味著需要給每一種 CPU 重寫匯編代碼。
JavaScirpt 引擎可以將 JS 代碼編譯為不同 CPU(Intel, ARM 以及 MIPS 等)對應(yīng)的匯編代碼,這樣我們就不需要去翻閱每個 CPU 的指令集手冊來編寫匯編代碼了。當(dāng)然,JavaScript 引擎的工作也不只是編譯代碼,它還要負責(zé)執(zhí)行代碼、分配內(nèi)存以及垃圾回收。
- 1000100111011000 #機器指令
- 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 年實現(xiàn)了編譯器和字節(jié)碼解釋器,升級為了 SquirrelFish。蘋果內(nèi)部代號為“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
- Rhino,由 Mozilla 基金會管理,開放源代碼,完全以 Java 編寫,用于 HTMLUnit
- SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,現(xiàn)時用于 Mozilla Firefox。
- Chakra (JScript 引擎),用于 Internet Explorer。
- Chakra (JavaScript 引擎),用于 Microsoft Edge。
- KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波頓開發(fā),用于 KDE 項目的 Konqueror 網(wǎng)頁瀏覽器中。
- JerryScript — 三星推出的適用于嵌入式設(shè)備的小型 JavaScript 引擎。
- 其他:Nashorn、QuickJS 、 Hermes
V8
Google V8 引擎是用 C ++編寫的開源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等??梢赃\行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 處理器的 Linux 系統(tǒng)上。V8 最早被開發(fā)用以嵌入到 Google 的開源瀏覽器 Chrome 中,第一個版本隨著第一版Chrome于 2008 年 9 月 2 日發(fā)布。但是 V8 是一個可以獨立運行的模塊,完全可以嵌入到任何 C ++應(yīng)用程序中。著名的 Node.js( 一個異步的服務(wù)器框架,可以在服務(wù)端使用 JavaScript 寫出高效的網(wǎng)絡(luò)服務(wù)器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。??
和其他 JavaScript 引擎一樣,V8 會編譯 / 執(zhí)行 JavaScript 代碼,管理內(nèi)存,負責(zé)垃圾回收,與宿主語言的交互等。通過暴露宿主對象 ( 變量,函數(shù)等 ) 到 JavaScript,JavaScript 可以訪問宿主環(huán)境中的對象,并在腳本中完成對宿主對象的操作。
與君初相識,猶如故人歸
什么是 D8
d8 是一個非常有用的調(diào)試工具,你可以把它看成是 debug for V8 的縮寫。我們可以使用 d8 來查看 V8 在執(zhí)行 JavaScript 過程中的各種中間數(shù)據(jù),比如作用域、AST、字節(jié)碼、優(yōu)化的二進制代碼、垃圾回收的狀態(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
- // 解壓文件,點擊d8打開(mac安全策略限制的話,按住control,再點擊,彈出菜單中選擇打開)
- 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 演示時的文件目錄結(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這種方式進行調(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)計數(shù)據(jù)
使用 d8 進行調(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)部方法,只需要在啟動 V8 時傳入 --allow-natives-syntax 命令,你就可以在 test.js 中使用諸如HasFastProperties(檢查一個對象是否擁有快屬性)的內(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);
- // 檢查一個對象是否擁有快屬性
- console.log(%HasFastProperties(bar));
- delete bar.property2;
- console.log(%HasFastProperties(bar));
- ./d8 --allow-natives-syntax ./test.js
- # 依次打?。簍rue false
心似雙絲網(wǎng),中有千千結(jié)
V8 引擎的內(nèi)部結(jié)構(gòu)
V8 是一個非常復(fù)雜的項目,有超過 100 萬行 C++代碼。它由許多子模塊構(gòu)成,其中這 4 個模塊是最重要的:
- Parser:負責(zé)將 JavaScript 源碼轉(zhuǎn)換為 Abstract Syntax Tree (AST)
- Ignition:interpreter,即解釋器,負責(zé)將 AST 轉(zhuǎn)換為 Bytecode,解釋執(zhí)行 Bytecode;同時收集 TurboFan 優(yōu)化編譯所需的信息,比如函數(shù)參數(shù)的類型;解釋器執(zhí)行時主要有四個模塊,內(nèi)存中的字節(jié)碼、寄存器、棧、堆。
通常有兩種類型的解釋器,基于棧 (Stack-based)和基于寄存器 (Register-based),基于棧的解釋器使用棧來保存函數(shù)參數(shù)、中間運算結(jié)果、變量等;基于寄存器的虛擬機則支持寄存器的指令操作,使用寄存器來保存參數(shù)、中間計算結(jié)果。通常,基于棧的虛擬機也定義了少量的寄存器,基于寄存器的虛擬機也有堆棧,其區(qū)別體現(xiàn)在它們提供的指令集體系。大多數(shù)解釋器都是基于棧的,比如 Java 虛擬機,.Net 虛擬機,還有早期的 V8 虛擬機?;诙褩5奶摂M機在處理函數(shù)調(diào)用、解決遞歸問題和切換上下文時簡單明快。而現(xiàn)在的 V8 虛擬機則采用了基于寄存器的設(shè)計,它將一些中間數(shù)據(jù)保存到寄存器中。
基于寄存器的解釋器架構(gòu):
- TurboFan:compiler,即編譯器,利用 Ignitio 所收集的類型信息,將 Bytecode 轉(zhuǎn)換為優(yōu)化的匯編代碼;
- Orinoco:garbage collector,垃圾回收模塊,負責(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ù)沒有被調(diào)用,則 V8 不會去編譯它。
- 如果函數(shù)只被調(diào)用 1 次,則 Ignition 將其編譯 Bytecode 就直接解釋執(zhí)行了。TurboFan 不會進行優(yōu)化編譯,因為它需要 Ignition 收集函數(shù)執(zhí)行時的類型信息。這就要求函數(shù)至少需要執(zhí)行 1 次,TurboFan 才有可能進行優(yōu)化編譯。
- 如果函數(shù)被調(diào)用多次,則它有可能會被識別為熱點函數(shù),且 Ignition 收集的類型信息證明可以進行優(yōu)化編譯的話,這時 TurboFan 則會將 Bytecode 編譯為 Optimized Machine Code(已優(yōu)化的機器碼),以提高代碼的執(zhí)行性能。??
圖片中的紅色虛線是逆向的,也就是說Optimized Machine Code 會被還原為 Bytecode,這個過程叫做 Deoptimization。這是因為 Ignition 收集的信息可能是錯誤的,比如 add 函數(shù)的參數(shù)之前是整數(shù),后來又變成了字符串。生成的 Optimized Machine Code 已經(jīng)假定 add 函數(shù)的參數(shù)是整數(shù),那當(dāng)然是錯誤的,于是需要進行 Deoptimization。
- function add(x, y) {
- return x + y;
- }
- add(3, 5);
- add('3', '5');
在運行 C、C++以及 Java 等程序之前,需要進行編譯,不能直接執(zhí)行源碼;但對于 JavaScript 來說,我們可以直接執(zhí)行源碼(比如:node test.js),它是在運行的時候先編譯再執(zhí)行,這種方式被稱為即時編譯(Just-in-time compilation),簡稱為 JIT。因此,V8 也屬于 JIT 編譯器。
V8 是怎么執(zhí)行一段 JavaScript 代碼的
- 在 V8 出現(xiàn)之前,所有的 JavaScript 虛擬機所采用的都是解釋執(zhí)行的方式,這是 JavaScript 執(zhí)行速度過慢的一個主要原因。而 V8 率先引入了即時編譯(JIT)的雙輪驅(qū)動的設(shè)計(混合使用編譯器和解釋器的技術(shù)),這是一種權(quán)衡策略,混合編譯執(zhí)行和解釋執(zhí)行這兩種手段,給 JavaScript 的執(zhí)行速度帶來了極大的提升。V8 出現(xiàn)之后,各大廠商也都在自己的 JavaScript 虛擬機中引入了 JIT 機制,所以目前市面上 JavaScript 虛擬機都有著類似的架構(gòu)。另外,V8 也是早于其他虛擬機引入了惰性編譯、內(nèi)聯(lián)緩存、隱藏類等機制,進一步優(yōu)化了 JavaScript 代碼的編譯執(zhí)行效率。
- V8 執(zhí)行一段 JavaScript 的流程圖:
- V8 本質(zhì)上是一個虛擬機,因為計算機只能識別二進制指令,所以要讓計算機執(zhí)行一段高級語言通常有兩種手段:
- 第一種是將高級代碼轉(zhuǎn)換為二進制代碼,再讓計算機去執(zhí)行;
- 另外一種方式是在計算機安裝一個解釋器,并由解釋器來解釋執(zhí)行。
- 解釋執(zhí)行和編譯執(zhí)行都有各自的優(yōu)缺點,解釋執(zhí)行啟動速度快,但是執(zhí)行時速度慢,而編譯執(zhí)行啟動速度慢,但是執(zhí)行速度快。為了充分地利用解釋執(zhí)行和編譯執(zhí)行的優(yōu)點,規(guī)避其缺點,V8 采用了一種權(quán)衡策略,在啟動過程中采用了解釋執(zhí)行的策略,但是如果某段代碼的執(zhí)行頻率超過一個值,那么 V8 就會采用優(yōu)化編譯器將其編譯成執(zhí)行效率更加高效的機器代碼。
- 總結(jié):
V8 執(zhí)行一段 JavaScript 代碼所經(jīng)歷的主要流程包括:
- 初始化基礎(chǔ)環(huán)境;
- 解析源碼生成 AST 和作用域;
- 依據(jù) AST 和作用域生成字節(jié)碼;
- 解釋執(zhí)行字節(jié)碼;
- 監(jiān)聽熱點代碼;
- 優(yōu)化熱點代碼為二進制的機器代碼;
- 反優(yōu)化生成的二進制機器代碼。
一等公民與閉包
一等公民的定義
- 在編程語言中,一等公民可以作為函數(shù)參數(shù),可以作為函數(shù)返回值,也可以賦值給變量。
- 如果某個編程語言的函數(shù),可以和這個語言的數(shù)據(jù)類型做一樣的事情,我們就把這個語言中的函數(shù)稱為一等公民。例如,字符串在幾乎所有編程語言中都是一等公民,字符串可以做為函數(shù)參數(shù),字符串可以作為函數(shù)返回值,字符串也可以賦值給變量。對于各種編程語言來說,函數(shù)就不一定是一等公民了,比如 Java 8 之前的版本。
- 對于 JavaScript 來說,函數(shù)可以賦值給變量,也可以作為函數(shù)參數(shù),還可以作為函數(shù)返回值,因此 JavaScript 中函數(shù)是一等公民。
動態(tài)作用域與靜態(tài)作用域
- 如果一門語言的作用域是靜態(tài)作用域,那么符號之間的引用關(guān)系能夠根據(jù)程序代碼在編譯時就確定清楚,在運行時不會變。某個函數(shù)是在哪聲明的,就具有它所在位置的作用域。它能夠訪問哪些變量,那么就跟這些變量綁定了,在運行時就一直能訪問這些變量。即靜態(tài)作用域可以由程序代碼決定,在編譯時就能完全確定。大多數(shù)語言都是靜態(tài)作用域的。
- 動態(tài)作用域(Dynamic Scope)。也就是說,變量引用跟變量聲明不是在編譯時就綁定死了的。在運行時,它是在運行環(huán)境中動態(tài)地找一個相同名稱的變量。在 macOS 或 Linux 中用的 bash 腳本語言,就是動態(tài)作用域的。
閉包的三個基礎(chǔ)特性
- JavaScript 語言允許在函數(shù)內(nèi)部定義新的函數(shù)
- 可以在內(nèi)部函數(shù)中訪問父函數(shù)中定義的變量
- 因為 JavaScript 中的函數(shù)是一等公民,所以函數(shù)可以作為另外一個函數(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 解析為中間代碼,這主要是基于以下兩點:
- 首先,如果一次解析和編譯所有的 JavaScript 代碼,過多的代碼會增加編譯時間,這會嚴重影響到首次執(zhí)行 JavaScript 代碼的速度,讓用戶感覺到卡頓。因為有時候一個頁面的 JavaScript 代碼很大,如果要將所有的代碼一次性解析編譯完成,那么會大大增加用戶的等待時間;
- 其次,解析完成的字節(jié)碼和編譯之后的機器代碼都會存放在內(nèi)存中,如果一次性解析和編譯所有 JavaScript 代碼,那么這些中間代碼和機器代碼將會一直占用內(nèi)存。
- 基于以上的原因,所有主流的 JavaScript 虛擬機都實現(xiàn)了惰性解析。
- 閉包給惰性解析帶來的問題:上文的 d 不能隨著 foo 函數(shù)的執(zhí)行上下文被銷毀掉。
預(yù)解析器
V8 引入預(yù)解析器,比如當(dāng)解析頂層代碼的時候,遇到了一個函數(shù),那么預(yù)解析器并不會直接跳過該函數(shù),而是對該函數(shù)做一次快速的預(yù)解析。
- 判斷當(dāng)前函數(shù)是不是存在一些語法上的錯誤,發(fā)現(xiàn)了語法錯誤,那么就會向 V8 拋出語法錯誤;
- 檢查函數(shù)內(nèi)部是否引用了外部變量,如果引用了外部的變量,預(yù)解析器會將棧中的變量復(fù)制到堆中,在下次執(zhí)行到該函數(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ù)字屬性稱為排序?qū)傩?,?V8 中被稱為 elements,字符串屬性就被稱為常規(guī)屬性,在 V8 中被稱為 properties。在 V8 內(nèi)部,為了有效地提升存儲和訪問這兩種屬性的性能,分別使用了兩個線性數(shù)據(jù)結(jié)構(gòu)來分別保存排序?qū)傩院统R?guī)屬性。同時 v8 將部分常規(guī)屬性直接存儲到對象本身,我們把這稱為對象內(nèi)屬性 (in-object properties),不過對象內(nèi)屬性的數(shù)量是固定的,默認是 10 個。
- 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)存快照。通過增大第一個參數(shù)來查看存儲變化。
我們將保存在線性數(shù)據(jù)結(jié)構(gòu)中的屬性稱之為“快屬性”,因為線性數(shù)據(jù)結(jié)構(gòu)中只需要通過索引即可以訪問到屬性,雖然訪問線性結(jié)構(gòu)的速度快,但是如果從線性結(jié)構(gòu)中添加或者刪除大量的屬性時,則執(zhí)行效率會非常低,這主要因為會產(chǎn)生大量時間和內(nèi)存開銷。因此,如果一個對象的屬性過多時,V8 就會采取另外一種存儲策略,那就是“慢屬性”策略,但慢屬性的對象內(nèi)部會有獨立的非線性數(shù)據(jù)結(jié)構(gòu) (字典) 作為屬性存儲容器。所有的屬性元信息不再是線性存儲的,而是直接保存在屬性字典中。
v8 屬性存儲:
總結(jié):??
因為 JavaScript 中的對象是由一組組屬性和值組成的,所以最簡單的方式是使用一個字典來保存屬性和值,但是由于字典是非線性結(jié)構(gòu),所以如果使用字典,讀取效率會大大降低。為了提升查找效率,V8 在對象中添加了兩個隱藏屬性,排序?qū)傩院统R?guī)屬性,element 屬性指向了 elements 對象,在 elements 對象中,會按照順序存放排序?qū)傩?。properties 屬性則指向了 properties 對象,在 properties 對象中,會按照創(chuàng)建時的順序保存常規(guī)屬性。??
通過引入這兩個屬性,加速了 V8 查找屬性的速度,為了更加進一步提升查找效率,V8 還實現(xiàn)了內(nèi)置內(nèi)屬性的策略,當(dāng)常規(guī)屬性少于一定數(shù)量時,V8 就會將這些常規(guī)屬性直接寫進對象中,這樣又節(jié)省了一個中間步驟。??
但是如果對象中的屬性過多時,或者存在反復(fù)添加或者刪除屬性的操作,那么 V8 就會將線性的存儲模式降級為非線性的字典存儲模式,這樣雖然降低了查找速度,但是卻提升了修改對象的屬性的速度。
堆空間和??臻g
??臻g
- 現(xiàn)代語言都是基于函數(shù)的,每個函數(shù)在執(zhí)行過程中,都有自己的生命周期和作用域,當(dāng)函數(shù)執(zhí)行結(jié)束時,其作用域也會被銷毀,因此,我們會使用棧這種數(shù)據(jù)結(jié)構(gòu)來管理函數(shù)的調(diào)用過程,我們也把管理函數(shù)調(diào)用過程的棧結(jié)構(gòu)稱之為調(diào)用棧。
- ??臻g主要是用來管理 JavaScript 函數(shù)調(diào)用的,棧是內(nèi)存中連續(xù)的一塊空間,同時棧結(jié)構(gòu)是“先進后出”的策略。在函數(shù)調(diào)用過程中,涉及到上下文相關(guān)的內(nèi)容都會存放在棧上,比如原生類型、引用到的對象的地址、函數(shù)的執(zhí)行狀態(tài)、this 值等都會存在在棧上。當(dāng)一個函數(shù)執(zhí)行結(jié)束,那么該函數(shù)的執(zhí)行上下文便會被銷毀掉。
- ??臻g的最大的特點是空間連續(xù),所以在棧中每個元素的地址都是固定的,因此??臻g的查找效率非常高,但是通常在內(nèi)存中,很難分配到一塊很大的連續(xù)空間,因此,V8 對棧空間的大小做了限制,如果函數(shù)調(diào)用層過深,那么 V8 就有可能拋出棧溢出的錯誤。
- 棧的優(yōu)勢和缺點:
- 棧的結(jié)構(gòu)非常適合函數(shù)調(diào)用過程。
- 在棧上分配資源和銷毀資源的速度非常快,這主要歸結(jié)于??臻g是連續(xù)的,分配空間和銷毀空間只需要移動下指針就可以了。
- 雖然操作速度非???,但是棧也是有缺點的,其中最大的缺點也是它的優(yōu)點所造成的,那就是棧是連續(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 對象等,這些都是存在堆空間的。
- 宿主在啟動 V8 的過程中,會同時創(chuàng)建堆空間和棧空間,再繼續(xù)往下執(zhí)行,產(chǎn)生的新數(shù)據(jù)都會存放在這兩個空間中。
繼承
繼承就是一個對象可以訪問另外一個對象中的屬性和方法,在 JavaScript 中,我們通過原型和原型鏈的方式來實現(xiàn)了繼承特性。
JavaScript 的每個對象都包含了一個隱藏屬性 __proto__ ,我們就把該隱藏屬性 __proto__ 稱之為該對象的原型 (prototype),__proto__ 指向了內(nèi)存中的另外一個對象,我們就把 __proto__ 指向的對象稱為該對象的原型對象,那么該對象就可以直接訪問其原型對象的方法或者屬性。??
JavaScript 中的繼承非常簡潔,就是每個對象都有一個原型屬性,該屬性指向了原型對象,查找屬性的時候,JavaScript 虛擬機會沿著原型一層一層向上查找,直至找到正確的屬性。
隱藏屬性__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__實現(xiàn)繼承:
- dog.__proto__ = animal;
- dog.getInfo();
通常隱藏屬性是不能使用 JavaScript 來直接與之交互的。雖然現(xiàn)代瀏覽器都開了一個口子,讓 JavaScript 可以訪問隱藏屬性 __proto__,但是在實際項目中,我們不應(yīng)該直接通過 __proto__ 來訪問或者修改該屬性,其主要原因有兩個:
- 首先,這是隱藏屬性,并不是標(biāo)準(zhǔ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)建對象和實現(xiàn)對象的繼承。不過使用這種方式隱含的語義過于隱晦。其實是 JavaScript 為了吸引 Java 程序員、在語法層面去蹭 Java 熱點,所以就被硬生生地強制加入了非常不協(xié)調(diào)的關(guān)鍵字 new。
- function DogFactory(type, color) {
- this.type = type;
- this.color = color;
- }
- var dog = new DogFactory('Dog', 'Black');
其實當(dāng) V8 執(zhí)行上面這段代碼時,V8 在背后悄悄地做了以下幾件事情:
- var dog = {};
- dog.__proto__ = DogFactory.prototype;
- DogFactory.call(dog, 'Dog', 'Black');
機器碼、字節(jié)碼
V8 為什么要引入字節(jié)碼
- 早期的 V8 為了提升代碼的執(zhí)行速度,直接將 JavaScript 源代碼編譯成了沒有優(yōu)化的二進制機器代碼,如果某一段二進制代碼執(zhí)行頻率過高,那么 V8 會將其標(biāo)記為熱點代碼,熱點代碼會被優(yōu)化編譯器優(yōu)化,優(yōu)化后的機器代碼執(zhí)行效率更高。
- 隨著移動設(shè)備的普及,V8 團隊逐漸發(fā)現(xiàn)將 JavaScript 源碼直接編譯成二進制代碼存在兩個致命的問題:
- 時間問題:編譯時間過久,影響代碼啟動速度;
- 空間問題:緩存編譯后的二進制代碼占用更多的內(nèi)存。
- 這兩個問題無疑會阻礙 V8 在移動設(shè)備上的普及,于是 V8 團隊大規(guī)模重構(gòu)代碼,引入了中間的字節(jié)碼。字節(jié)碼的優(yōu)勢有如下三點:
- 解決啟動問題:生成字節(jié)碼的時間很短;
- 解決空間問題:字節(jié)碼雖然占用的空間比原始的 JavaScript 多,但是相較于機器代碼,字節(jié)碼還是小了太多,緩存字節(jié)碼會大大降低內(nèi)存的使用。
- 代碼架構(gòu)清晰:采用字節(jié)碼,可以簡化程序的復(fù)雜度,使得 V8 移植到不同的 CPU 架構(gòu)平臺更加容易。
- Bytecode 某種程度上就是匯編語言,只是它沒有對應(yīng)特定的 CPU,或者說它對應(yīng)的是虛擬的 CPU。這樣的話,生成 Bytecode 時簡單很多,無需為不同的 CPU 生產(chǎn)不同的代碼。要知道,V8 支持 9 種不同的 CPU,引入一個中間層 Bytecode,可以簡化 V8 的編譯流程,提高可擴展性。
- 如果我們在不同硬件上去生成 Bytecode,會發(fā)現(xiàn)生成代碼的指令是一樣的。
如何查看字節(jié)碼
- // test.js
- function add(x, y) {
- var z = x + y;
- return z;
- }
- console.log(add(1, 2));
運行./d8 ./test.js --print-bytecode:
- [generated bytecode for function: add (0x01000824fe59
)] - Parameter count 3 #三個參數(shù),包括了顯式地傳入的 x 和 y,還有一個隱式地傳入的 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,就是把某個寄存器中的值,加載到累加器中。
- Star:表示 Store Accumulator Register, 你可以把它理解為 Store Accumulator to Register,就是把累加器中的值保存到某個寄存器中
- Add:Add a0, [0]是從 a0 寄存器加載值并將其與累加器中的值相加,然后將結(jié)果再次放入累加器。
add a0 后面的[0]稱之為 feedback vector slot,又叫反饋向量槽,它是一個數(shù)組,解釋器將解釋執(zhí)行過程中的一些數(shù)據(jù)類型的分析信息都保存在這個反饋向量槽中了,目的是為了給 TurboFan 優(yōu)化編譯器提供優(yōu)化信息,很多字節(jié)碼都會為反饋向量槽提供運行時信息。
- LdaSmi:將小整數(shù)(Smi)加載到累加器寄存器中
- Return:結(jié)束當(dāng)前函數(shù)的執(zhí)行,并將控制權(quán)傳回給調(diào)用方。返回的值是累加器中的值。
隱藏類和內(nèi)聯(lián)緩存
JavaScript 是一門動態(tài)語言,其執(zhí)行效率要低于靜態(tài)語言,V8 為了提升 JavaScript 的執(zhí)行速度,借鑒了很多靜態(tài)語言的特性,比如實現(xiàn)了 JIT 機制,為了提升對象的屬性訪問速度而引入了隱藏類,為了加速運算而引入了內(nèi)聯(lián)緩存。
為什么靜態(tài)語言的效率更高???
靜態(tài)語言中,如 C++ 在聲明一個對象之前需要定義該對象的結(jié)構(gòu),代碼在執(zhí)行之前需要先被編譯,編譯的時候,每個對象的形狀都是固定的,也就是說,在代碼的執(zhí)行過程中是無法被改變的??梢灾苯油ㄟ^偏移量查詢來查詢對象的屬性值,這也就是靜態(tài)語言的執(zhí)行效率高的一個原因。??
JavaScript 在運行時,對象的屬性是可以被修改的,所以當(dāng) V8 使用了一個對象時,比如使用了 obj.x 的時候,它并不知道該對象中是否有 x,也不知道 x 相對于對象的偏移量是多少,也就是說 V8 并不知道該對象的具體的形狀。那么,當(dāng)在 JavaScript 中要查詢對象 obj 中的 x 屬性時,V8 會按照具體的規(guī)則一步一步來查詢,這個過程非常的慢且耗時。
將靜態(tài)的特性引入到 V8
- V8 采用的一個思路就是將 JavaScript 中的對象靜態(tài)化,也就是 V8 在運行 JavaScript 的過程中,會假設(shè) JavaScript 中的對象是靜態(tài)的。
- 具體地講,V8 對每個對象做如下兩點假設(shè):
- 對象創(chuàng)建好了之后就不會添加新的屬性;
- 對象創(chuàng)建好了之后也不會刪除屬性。
- 符合這兩個假設(shè)之后,V8 就可以對 JavaScript 中的對象做深度優(yōu)化了。V8 會為每個對象創(chuàng)建一個隱藏類,對象的隱藏類中記錄了該對象一些基礎(chǔ)的布局信息,包括以下兩點:
- 對象中所包含的所有的屬性;
- 每個屬性相對于對象的偏移量。
- 有了隱藏類之后,那么當(dāng) V8 訪問某個對象中的某個屬性時,就會先去隱藏類中查找該屬性相對于它的對象的偏移量,有了偏移量和屬性類型,V8 就可以直接去內(nèi)存中取出對應(yīng)的屬性值,而不需要經(jīng)歷一系列的查找過程,那么這就大大提升了 V8 查找對象的效率。
- 在 V8 中,把隱藏類又稱為 map,每個對象都有一個 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
多個對象共用一個隱藏類
- 在 V8 中,每個對象都有一個 map 屬性,該屬性值指向該對象的隱藏類。不過如果兩個對象的形狀是相同的,V8 就會為其復(fù)用同一個隱藏類,這樣有兩個好處:
- 減少隱藏類的創(chuàng)建次數(shù),也間接加速了代碼的執(zhí)行速度;
- 減少了隱藏類的存儲空間。
- 那么,什么情況下兩個對象的形狀是相同的,要滿足以下兩點:
- 相同的屬性名稱;
- 相等的屬性個數(shù)。
重新構(gòu)建隱藏類
- 給一個對象添加新的屬性,刪除新的屬性,或者改變某個屬性的數(shù)據(jù)類型都會改變這個對象的形狀,那么勢必也就會觸發(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


咨詢
建站咨詢
