溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

發(fā)布時(shí)間:2021-09-30 13:42:52 來(lái)源:億速云 閱讀:177 作者:柒染 欄目:web開(kāi)發(fā)

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法,相信很多沒(méi)有經(jīng)驗(yàn)的人對(duì)此束手無(wú)策,為此本文總結(jié)了問(wèn)題出現(xiàn)的原因和解決方法,通過(guò)這篇文章希望你能解決這個(gè)問(wèn)題。

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

為什么需要 JavaScript 引擎

我們寫(xiě)的 JavaScript 代碼直接交給瀏覽器或者 Node 執(zhí)行時(shí),底層的 CPU 是不認(rèn)識(shí)的,也沒(méi)法執(zhí)行。CPU 只認(rèn)識(shí)自己的指令集,指令集對(duì)應(yīng)的是匯編代碼。寫(xiě)匯編代碼是一件很痛苦的事情。并且不同類型的 CPU 的指令集是不一樣的,那就意味著需要給每一種 CPU 重寫(xiě)匯編代碼。

JavaScirpt 引擎可以將 JS 代碼編譯為不同 CPU(Intel, ARM 以及 MIPS 等)對(duì)應(yīng)的匯編代碼,這樣我們就不需要去翻閱每個(gè) CPU 的指令集手冊(cè)來(lái)編寫(xiě)匯編代碼了。當(dāng)然,JavaScript 引擎的工作也不只是編譯代碼,它還要負(fù)責(zé)執(zhí)行代碼、分配內(nèi)存以及垃圾回收。

1000100111011000  #機(jī)器指令  mov ax,bx         #匯編指令

資料拓展: 匯編語(yǔ)言入門教程【阮一峰】 | 理解 V8 的字節(jié)碼「譯」

https://zhuanlan.zhihu.com/p/28590489

熱門 JavaScript 引擎

  •  V8 (Google),用 C++編寫(xiě),開(kāi)放源代碼,由 Google 丹麥開(kāi)發(fā),是 Google Chrome 的一部分,也用于 Node.js。

  •  JavaScriptCore (Apple),開(kāi)放源代碼,用于 webkit 型瀏覽器,如 Safari ,2008 年實(shí)現(xiàn)了編譯器和字節(jié)碼解釋器,升級(jí)為了 SquirrelFish。蘋(píng)果內(nèi)部代號(hào)為“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。

  •  Rhino,由 Mozilla 基金會(huì)管理,開(kāi)放源代碼,完全以 Java 編寫(xiě),用于 HTMLUnit

  •  SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,現(xiàn)時(shí)用于 Mozilla Firefox。

  •  Chakra (JScript 引擎),用于 Internet Explorer。

  •  Chakra (JavaScript 引擎),用于 Microsoft Edge。

  •  KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波頓開(kāi)發(fā),用于 KDE 項(xiàng)目的 Konqueror 網(wǎng)頁(yè)瀏覽器中。

  •  JerryScript — 三星推出的適用于嵌入式設(shè)備的小型 JavaScript 引擎。

  •  其他:Nashorn、QuickJS 、 Hermes

V8

Google V8 引擎是用 C ++編寫(xiě)的開(kāi)源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以運(yùn)行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 處理器的 Linux 系統(tǒng)上。V8 最早被開(kāi)發(fā)用以嵌入到 Google 的開(kāi)源瀏覽器 Chrome 中,第一個(gè)版本隨著第一版Chrome于 2008 年 9 月 2 日發(fā)布。但是 V8 是一個(gè)可以獨(dú)立運(yùn)行的模塊,完全可以嵌入到任何 C ++應(yīng)用程序中。著名的 Node.js( 一個(gè)異步的服務(wù)器框架,可以在服務(wù)端使用 JavaScript 寫(xiě)出高效的網(wǎng)絡(luò)服務(wù)器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。  

和其他 JavaScript 引擎一樣,V8 會(huì)編譯 / 執(zhí)行 JavaScript 代碼,管理內(nèi)存,負(fù)責(zé)垃圾回收,與宿主語(yǔ)言的交互等。通過(guò)暴露宿主對(duì)象 ( 變量,函數(shù)等 ) 到 JavaScript,JavaScript 可以訪問(wèn)宿主環(huán)境中的對(duì)象,并在腳本中完成對(duì)宿主對(duì)象的操作。

什么是 D8

d8 是一個(gè)非常有用的調(diào)試工具,你可以把它看成是 debug for V8 的縮寫(xiě)。我們可以使用 d8 來(lái)查看 V8 在執(zhí)行 JavaScript 過(guò)程中的各種中間數(shù)據(jù),比如作用域、AST、字節(jié)碼、優(yōu)化的二進(jìn)制代碼、垃圾回收的狀態(tài),還可以使用 d8 提供的私有 API 查看一些內(nèi)部信息。

安裝 D8

  •  方法一:自行下載編譯

    •  v8 google 下載及編譯使用

    •  官方文檔:Using d8

  •  方法二:使用編譯好的 d8 工具

  •  mac 平臺(tái):

https://storage.googleapis.com/chromium-v8/official/canary/v8-mac64-dbg-8.4.109.zip

  •  linux32 平臺(tái):

https://storage.googleapis.com/chromium-v8/official/canary/v8-linux32-dbg-8.4.109.zip

  •  linux64 平臺(tái):

https://storage.googleapis.com/chromium-v8/official/canary/v8-linux64-dbg-8.4.109.zip

  • win32 平臺(tái):

https://storage.googleapis.com/chromium-v8/official/canary/v8-win32-dbg-8.4.109.zip

  • win64 平臺(tái):

https://storage.googleapis.com/chromium-v8/official/canary/v8-win64-dbg-8.4.109.zip

// 解壓文件,點(diǎn)擊d8打開(kāi)(mac安全策略限制的話,按住control,再點(diǎn)擊,彈出菜單中選擇打開(kāi))    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
  •  過(guò)濾特定的命令,如: 

# 如果是 Windows 系統(tǒng),可能缺少 grep 程序,請(qǐng)自行下載安裝并添加環(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í)行下面這行命令,就會(huì)打印出 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è)對(duì)象是否擁有快屬性)的內(nèi)部方法(索引屬性、常規(guī)屬性、快屬性等下文會(huì)介紹)。

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è)對(duì)象是否擁有快屬性  console.log(%HasFastProperties(bar));  delete bar.property2;  console.log(%HasFastProperties(bar));
./d8 --allow-natives-syntax ./test.js  # 依次打?。簍rue false

V8 引擎的內(nèi)部結(jié)構(gòu)

V8 是一個(gè)非常復(fù)雜的項(xiàng)目,有超過(guò) 100 萬(wàn)行 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),基于棧的解釋器使用棧來(lái)保存函數(shù)參數(shù)、中間運(yùn)算結(jié)果、變量等;基于寄存器的虛擬機(jī)則支持寄存器的指令操作,使用寄存器來(lái)保存參數(shù)、中間計(jì)算結(jié)果。通常,基于棧的虛擬機(jī)也定義了少量的寄存器,基于寄存器的虛擬機(jī)也有堆棧,其區(qū)別體現(xiàn)在它們提供的指令集體系。大多數(shù)解釋器都是基于棧的,比如 Java 虛擬機(jī),.Net 虛擬機(jī),還有早期的 V8 虛擬機(jī)。基于堆棧的虛擬機(jī)在處理函數(shù)調(diào)用、解決遞歸問(wèn)題和切換上下文時(shí)簡(jiǎn)單明快。而現(xiàn)在的 V8 虛擬機(jī)則采用了基于寄存器的設(shè)計(jì),它將一些中間數(shù)據(jù)保存到寄存器中。

基于寄存器的解釋器架構(gòu): 

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

  •  TurboFan:compiler,即編譯器,利用 Ignitio 所收集的類型信息,將 Bytecode 轉(zhuǎn)換為優(yōu)化的匯編代碼;

  •  Orinoco:garbage collector,垃圾回收模塊,負(fù)責(zé)將程序不再需要的內(nèi)存空間回收。

其中,Parser,Ignition 以及 TurboFan 可以將 JS 源碼編譯為匯編代碼,其流程圖如下:

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法&emsp;&emsp;

簡(jiǎn)單地說(shuō),Parser 將 JS 源碼轉(zhuǎn)換為 AST,然后 Ignition 將 AST 轉(zhuǎn)換為 Bytecode,最后 TurboFan 將 Bytecode 轉(zhuǎn)換為經(jīng)過(guò)優(yōu)化的 Machine Code(實(shí)際上是匯編代碼)。

  •  如果函數(shù)沒(méi)有被調(diào)用,則 V8 不會(huì)去編譯它。

  •  如果函數(shù)只被調(diào)用 1 次,則 Ignition 將其編譯 Bytecode 就直接解釋執(zhí)行了。TurboFan 不會(huì)進(jìn)行優(yōu)化編譯,因?yàn)樗枰?Ignition 收集函數(shù)執(zhí)行時(shí)的類型信息。這就要求函數(shù)至少需要執(zhí)行 1 次,TurboFan 才有可能進(jìn)行優(yōu)化編譯。

  •  如果函數(shù)被調(diào)用多次,則它有可能會(huì)被識(shí)別為熱點(diǎn)函數(shù),且 Ignition 收集的類型信息證明可以進(jìn)行優(yōu)化編譯的話,這時(shí) TurboFan 則會(huì)將 Bytecode 編譯為 Optimized Machine Code(已優(yōu)化的機(jī)器碼),以提高代碼的執(zhí)行性能。&emsp;&emsp;

圖片中的紅色虛線是逆向的,也就是說(shuō)Optimized Machine Code 會(huì)被還原為 Bytecode,這個(gè)過(guò)程叫做 Deoptimization。這是因?yàn)?Ignition 收集的信息可能是錯(cuò)誤的,比如 add 函數(shù)的參數(shù)之前是整數(shù),后來(lái)又變成了字符串。生成的 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í)行源碼;但對(duì)于 JavaScript 來(lái)說(shuō),我們可以直接執(zhí)行源碼(比如:node test.js),它是在運(yùn)行的時(shí)候先編譯再執(zhí)行,這種方式被稱為即時(shí)編譯(Just-in-time compilation),簡(jiǎn)稱為 JIT。因此,V8 也屬于 JIT 編譯器。

V8 是怎么執(zhí)行一段 JavaScript 代碼的

  •  在 V8 出現(xiàn)之前,所有的 JavaScript 虛擬機(jī)所采用的都是解釋執(zhí)行的方式,這是 JavaScript 執(zhí)行速度過(guò)慢的一個(gè)主要原因。而 V8 率先引入了即時(shí)編譯(JIT)的雙輪驅(qū)動(dòng)的設(shè)計(jì)(混合使用編譯器和解釋器的技術(shù)),這是一種權(quán)衡策略,混合編譯執(zhí)行和解釋執(zhí)行這兩種手段,給 JavaScript 的執(zhí)行速度帶來(lái)了極大的提升。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 的流程圖:    

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

  •  V8 本質(zhì)上是一個(gè)虛擬機(jī),因?yàn)橛?jì)算機(jī)只能識(shí)別二進(jìn)制指令,所以要讓計(jì)算機(jī)執(zhí)行一段高級(jí)語(yǔ)言通常有兩種手段:

    •  第一種是將高級(jí)代碼轉(zhuǎn)換為二進(jìn)制代碼,再讓計(jì)算機(jī)去執(zhí)行;

    •  另外一種方式是在計(jì)算機(jī)安裝一個(gè)解釋器,并由解釋器來(lái)解釋執(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)過(guò)程中采用了解釋執(zhí)行的策略,但是如果某段代碼的執(zhí)行頻率超過(guò)一個(gè)值,那么 V8 就會(huì)采用優(yōu)化編譯器將其編譯成執(zhí)行效率更加高效的機(jī)器代碼。

  •  總結(jié):

V8 執(zhí)行一段 JavaScript 代碼所經(jīng)歷的主要流程包括:

  • 初始化基礎(chǔ)環(huán)境;

  • 解析源碼生成 AST 和作用域;

  • 依據(jù) AST 和作用域生成字節(jié)碼;

  • 解釋執(zhí)行字節(jié)碼;

  • 監(jiān)聽(tīng)熱點(diǎn)代碼;

  • 優(yōu)化熱點(diǎn)代碼為二進(jìn)制的機(jī)器代碼;

  • 反優(yōu)化生成的二進(jìn)制機(jī)器代碼。

一等公民與閉包

一等公民的定義

  •  在編程語(yǔ)言中,一等公民可以作為函數(shù)參數(shù),可以作為函數(shù)返回值,也可以賦值給變量。

  •  如果某個(gè)編程語(yǔ)言的函數(shù),可以和這個(gè)語(yǔ)言的數(shù)據(jù)類型做一樣的事情,我們就把這個(gè)語(yǔ)言中的函數(shù)稱為一等公民。例如,字符串在幾乎所有編程語(yǔ)言中都是一等公民,字符串可以做為函數(shù)參數(shù),字符串可以作為函數(shù)返回值,字符串也可以賦值給變量。對(duì)于各種編程語(yǔ)言來(lái)說(shuō),函數(shù)就不一定是一等公民了,比如 Java 8 之前的版本。

  •  對(duì)于 JavaScript 來(lái)說(shuō),函數(shù)可以賦值給變量,也可以作為函數(shù)參數(shù),還可以作為函數(shù)返回值,因此 JavaScript 中函數(shù)是一等公民。

動(dòng)態(tài)作用域與靜態(tài)作用域

  •  如果一門語(yǔ)言的作用域是靜態(tài)作用域,那么符號(hào)之間的引用關(guān)系能夠根據(jù)程序代碼在編譯時(shí)就確定清楚,在運(yùn)行時(shí)不會(huì)變。某個(gè)函數(shù)是在哪聲明的,就具有它所在位置的作用域。它能夠訪問(wèn)哪些變量,那么就跟這些變量綁定了,在運(yùn)行時(shí)就一直能訪問(wèn)這些變量。即靜態(tài)作用域可以由程序代碼決定,在編譯時(shí)就能完全確定。大多數(shù)語(yǔ)言都是靜態(tài)作用域的。

  •  動(dòng)態(tài)作用域(Dynamic Scope)。也就是說(shuō),變量引用跟變量聲明不是在編譯時(shí)就綁定死了的。在運(yùn)行時(shí),它是在運(yùn)行環(huán)境中動(dòng)態(tài)地找一個(gè)相同名稱的變量。在 macOS 或 Linux 中用的 bash 腳本語(yǔ)言,就是動(dòng)態(tài)作用域的。

閉包的三個(gè)基礎(chǔ)特性

  •  JavaScript 語(yǔ)言允許在函數(shù)內(nèi)部定義新的函數(shù)

  •  可以在內(nèi)部函數(shù)中訪問(wèn)父函數(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 帶來(lái)的問(wèn)題及其解決策略。

惰性解析&emsp;&emsp;

所謂惰性解析是指解析器在解析的過(guò)程中,如果遇到函數(shù)聲明,那么會(huì)跳過(guò)函數(shù)內(nèi)部的代碼,并不會(huì)為其生成 AST 和字節(jié)碼,而僅僅生成頂層代碼的 AST 和字節(jié)碼。

  • 在編譯 JavaScript 代碼的過(guò)程中,V8 并不會(huì)一次性將所有的 JavaScript 解析為中間代碼,這主要是基于以下兩點(diǎn):

    •  首先,如果一次解析和編譯所有的 JavaScript 代碼,過(guò)多的代碼會(huì)增加編譯時(shí)間,這會(huì)嚴(yán)重影響到首次執(zhí)行 JavaScript 代碼的速度,讓用戶感覺(jué)到卡頓。因?yàn)橛袝r(shí)候一個(gè)頁(yè)面的 JavaScript 代碼很大,如果要將所有的代碼一次性解析編譯完成,那么會(huì)大大增加用戶的等待時(shí)間;

    • 其次,解析完成的字節(jié)碼和編譯之后的機(jī)器代碼都會(huì)存放在內(nèi)存中,如果一次性解析和編譯所有 JavaScript 代碼,那么這些中間代碼和機(jī)器代碼將會(huì)一直占用內(nèi)存。

  • 基于以上的原因,所有主流的 JavaScript 虛擬機(jī)都實(shí)現(xiàn)了惰性解析。

  • 閉包給惰性解析帶來(lái)的問(wèn)題:上文的 d 不能隨著 foo 函數(shù)的執(zhí)行上下文被銷毀掉。

預(yù)解析器

V8 引入預(yù)解析器,比如當(dāng)解析頂層代碼的時(shí)候,遇到了一個(gè)函數(shù),那么預(yù)解析器并不會(huì)直接跳過(guò)該函數(shù),而是對(duì)該函數(shù)做一次快速的預(yù)解析。

  •  判斷當(dāng)前函數(shù)是不是存在一些語(yǔ)法上的錯(cuò)誤,發(fā)現(xiàn)了語(yǔ)法錯(cuò)誤,那么就會(huì)向 V8 拋出語(yǔ)法錯(cuò)誤;

  •  檢查函數(shù)內(nèi)部是否引用了外部變量,如果引用了外部的變量,預(yù)解析器會(huì)將棧中的變量復(fù)制到堆中,在下次執(zhí)行到該函數(shù)的時(shí)候,直接使用堆中的引用,這樣就解決了閉包所帶來(lái)的問(wèn)題。

V8 內(nèi)部是如何存儲(chǔ)對(duì)象的:快屬性和慢屬性

下面的代碼會(huì)輸出什么:

// 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í)的順序升序排列。在這里我們把對(duì)象中的數(shù)字屬性稱為排序?qū)傩?,?V8 中被稱為 elements,字符串屬性就被稱為常規(guī)屬性,在 V8 中被稱為 properties。在 V8 內(nèi)部,為了有效地提升存儲(chǔ)和訪問(wèn)這兩種屬性的性能,分別使用了兩個(gè)線性數(shù)據(jù)結(jié)構(gòu)來(lái)分別保存排序?qū)傩院统R?guī)屬性。同時(shí) v8 將部分常規(guī)屬性直接存儲(chǔ)到對(duì)象本身,我們把這稱為對(duì)象內(nèi)屬性 (in-object properties),不過(guò)對(duì)象內(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);

可以通過(guò) Chrome 開(kāi)發(fā)者工具的 Memory 標(biāo)簽,捕獲查看當(dāng)前的內(nèi)存快照。通過(guò)增大第一個(gè)參數(shù)來(lái)查看存儲(chǔ)變化。

我們將保存在線性數(shù)據(jù)結(jié)構(gòu)中的屬性稱之為“快屬性”,因?yàn)榫€性數(shù)據(jù)結(jié)構(gòu)中只需要通過(guò)索引即可以訪問(wèn)到屬性,雖然訪問(wèn)線性結(jié)構(gòu)的速度快,但是如果從線性結(jié)構(gòu)中添加或者刪除大量的屬性時(shí),則執(zhí)行效率會(huì)非常低,這主要因?yàn)闀?huì)產(chǎn)生大量時(shí)間和內(nèi)存開(kāi)銷。因此,如果一個(gè)對(duì)象的屬性過(guò)多時(shí),V8 就會(huì)采取另外一種存儲(chǔ)策略,那就是“慢屬性”策略,但慢屬性的對(duì)象內(nèi)部會(huì)有獨(dú)立的非線性數(shù)據(jù)結(jié)構(gòu) (字典) 作為屬性存儲(chǔ)容器。所有的屬性元信息不再是線性存儲(chǔ)的,而是直接保存在屬性字典中。

v8 屬性存儲(chǔ):

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

總結(jié):&emsp;&emsp;

因?yàn)?JavaScript 中的對(duì)象是由一組組屬性和值組成的,所以最簡(jiǎn)單的方式是使用一個(gè)字典來(lái)保存屬性和值,但是由于字典是非線性結(jié)構(gòu),所以如果使用字典,讀取效率會(huì)大大降低。為了提升查找效率,V8 在對(duì)象中添加了兩個(gè)隱藏屬性,排序?qū)傩院统R?guī)屬性,element 屬性指向了 elements 對(duì)象,在 elements 對(duì)象中,會(huì)按照順序存放排序?qū)傩?。properties 屬性則指向了 properties 對(duì)象,在 properties 對(duì)象中,會(huì)按照創(chuàng)建時(shí)的順序保存常規(guī)屬性。&emsp;&emsp;

通過(guò)引入這兩個(gè)屬性,加速了 V8 查找屬性的速度,為了更加進(jìn)一步提升查找效率,V8 還實(shí)現(xiàn)了內(nèi)置內(nèi)屬性的策略,當(dāng)常規(guī)屬性少于一定數(shù)量時(shí),V8 就會(huì)將這些常規(guī)屬性直接寫(xiě)進(jìn)對(duì)象中,這樣又節(jié)省了一個(gè)中間步驟。&emsp;&emsp;

但是如果對(duì)象中的屬性過(guò)多時(shí),或者存在反復(fù)添加或者刪除屬性的操作,那么 V8 就會(huì)將線性的存儲(chǔ)模式降級(jí)為非線性的字典存儲(chǔ)模式,這樣雖然降低了查找速度,但是卻提升了修改對(duì)象的屬性的速度。

堆空間和??臻g

??臻g

  •  現(xiàn)代語(yǔ)言都是基于函數(shù)的,每個(gè)函數(shù)在執(zhí)行過(guò)程中,都有自己的生命周期和作用域,當(dāng)函數(shù)執(zhí)行結(jié)束時(shí),其作用域也會(huì)被銷毀,因此,我們會(huì)使用棧這種數(shù)據(jù)結(jié)構(gòu)來(lái)管理函數(shù)的調(diào)用過(guò)程,我們也把管理函數(shù)調(diào)用過(guò)程的棧結(jié)構(gòu)稱之為調(diào)用棧。

  •  ??臻g主要是用來(lái)管理 JavaScript 函數(shù)調(diào)用的,棧是內(nèi)存中連續(xù)的一塊空間,同時(shí)棧結(jié)構(gòu)是“先進(jìn)后出”的策略。在函數(shù)調(diào)用過(guò)程中,涉及到上下文相關(guān)的內(nèi)容都會(huì)存放在棧上,比如原生類型、引用到的對(duì)象的地址、函數(shù)的執(zhí)行狀態(tài)、this 值等都會(huì)存在在棧上。當(dāng)一個(gè)函數(shù)執(zhí)行結(jié)束,那么該函數(shù)的執(zhí)行上下文便會(huì)被銷毀掉。

  •  ??臻g的最大的特點(diǎn)是空間連續(xù),所以在棧中每個(gè)元素的地址都是固定的,因此??臻g的查找效率非常高,但是通常在內(nèi)存中,很難分配到一塊很大的連續(xù)空間,因此,V8 對(duì)棧空間的大小做了限制,如果函數(shù)調(diào)用層過(guò)深,那么 V8 就有可能拋出棧溢出的錯(cuò)誤。

  •  棧的優(yōu)勢(shì)和缺點(diǎn):

    • 棧的結(jié)構(gòu)非常適合函數(shù)調(diào)用過(guò)程。

    • 在棧上分配資源和銷毀資源的速度非常快,這主要?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));

堆空間

  •  堆空間是一種樹(shù)形的存儲(chǔ)結(jié)構(gòu),用來(lái)存儲(chǔ)對(duì)象類型的離散的數(shù)據(jù),JavaScript 中除了原生類型的數(shù)據(jù),其他的都是對(duì)象類型,諸如函數(shù)、數(shù)組,在瀏覽器中還有 window 對(duì)象、document 對(duì)象等,這些都是存在堆空間的。

  •  宿主在啟動(dòng) V8 的過(guò)程中,會(huì)同時(shí)創(chuàng)建堆空間和??臻g,再繼續(xù)往下執(zhí)行,產(chǎn)生的新數(shù)據(jù)都會(huì)存放在這兩個(gè)空間中。

繼承

繼承就是一個(gè)對(duì)象可以訪問(wèn)另外一個(gè)對(duì)象中的屬性和方法,在 JavaScript 中,我們通過(guò)原型和原型鏈的方式來(lái)實(shí)現(xiàn)了繼承特性。

JavaScript 的每個(gè)對(duì)象都包含了一個(gè)隱藏屬性 __proto__ ,我們就把該隱藏屬性 __proto__ 稱之為該對(duì)象的原型 (prototype),__proto__ 指向了內(nèi)存中的另外一個(gè)對(duì)象,我們就把 __proto__ 指向的對(duì)象稱為該對(duì)象的原型對(duì)象,那么該對(duì)象就可以直接訪問(wèn)其原型對(duì)象的方法或者屬性。&emsp;&emsp;

JavaScript 中的繼承非常簡(jiǎn)潔,就是每個(gè)對(duì)象都有一個(gè)原型屬性,該屬性指向了原型對(duì)象,查找屬性的時(shí)候,JavaScript 虛擬機(jī)會(huì)沿著原型一層一層向上查找,直至找到正確的屬性。

隱藏屬性__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 來(lái)直接與之交互的。雖然現(xiàn)代瀏覽器都開(kāi)了一個(gè)口子,讓 JavaScript 可以訪問(wèn)隱藏屬性 __proto__,但是在實(shí)際項(xiàng)目中,我們不應(yīng)該直接通過(guò) __proto__ 來(lái)訪問(wèn)或者修改該屬性,其主要原因有兩個(gè):

  •  首先,這是隱藏屬性,并不是標(biāo)準(zhǔn)定義的;

  •  其次,使用該屬性會(huì)造成嚴(yán)重的性能問(wèn)題。因?yàn)?JavaScript 通過(guò)隱藏類優(yōu)化了很多原有的對(duì)象結(jié)構(gòu),所以通過(guò)直接修改__proto__會(huì)直接破壞現(xiàn)有已經(jīng)優(yōu)化的結(jié)構(gòu),觸發(fā) V8 重構(gòu)該對(duì)象的隱藏類!

構(gòu)造函數(shù)是怎么創(chuàng)建對(duì)象的?&emsp;

在 JavaScript 中,使用 new 加上構(gòu)造函數(shù)的這種組合來(lái)創(chuàng)建對(duì)象和實(shí)現(xiàn)對(duì)象的繼承。不過(guò)使用這種方式隱含的語(yǔ)義過(guò)于隱晦。其實(shí)是 JavaScript 為了吸引 Java 程序員、在語(yǔ)法層面去蹭 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 源代碼編譯成了沒(méi)有優(yōu)化的二進(jìn)制機(jī)器代碼,如果某一段二進(jìn)制代碼執(zhí)行頻率過(guò)高,那么 V8 會(huì)將其標(biāo)記為熱點(diǎn)代碼,熱點(diǎn)代碼會(huì)被優(yōu)化編譯器優(yōu)化,優(yōu)化后的機(jī)器代碼執(zhí)行效率更高。

  •  隨著移動(dòng)設(shè)備的普及,V8 團(tuán)隊(duì)逐漸發(fā)現(xiàn)將 JavaScript 源碼直接編譯成二進(jìn)制代碼存在兩個(gè)致命的問(wèn)題:

    • 時(shí)間問(wèn)題:編譯時(shí)間過(guò)久,影響代碼啟動(dòng)速度;

    • 空間問(wèn)題:緩存編譯后的二進(jìn)制代碼占用更多的內(nèi)存。

  •  這兩個(gè)問(wèn)題無(wú)疑會(huì)阻礙 V8 在移動(dòng)設(shè)備上的普及,于是 V8 團(tuán)隊(duì)大規(guī)模重構(gòu)代碼,引入了中間的字節(jié)碼。字節(jié)碼的優(yōu)勢(shì)有如下三點(diǎn):

    •  解決啟動(dòng)問(wèn)題:生成字節(jié)碼的時(shí)間很短;

    •  解決空間問(wèn)題:字節(jié)碼雖然占用的空間比原始的 JavaScript 多,但是相較于機(jī)器代碼,字節(jié)碼還是小了太多,緩存字節(jié)碼會(huì)大大降低內(nèi)存的使用。

    •  代碼架構(gòu)清晰:采用字節(jié)碼,可以簡(jiǎn)化程序的復(fù)雜度,使得 V8 移植到不同的 CPU 架構(gòu)平臺(tái)更加容易。

  •  Bytecode 某種程度上就是匯編語(yǔ)言,只是它沒(méi)有對(duì)應(yīng)特定的 CPU,或者說(shuō)它對(duì)應(yīng)的是虛擬的 CPU。這樣的話,生成 Bytecode 時(shí)簡(jiǎn)單很多,無(wú)需為不同的 CPU 生產(chǎn)不同的代碼。要知道,V8 支持 9 種不同的 CPU,引入一個(gè)中間層 Bytecode,可以簡(jiǎn)化 V8 的編譯流程,提高可擴(kuò)展性。

  •  如果我們?cè)诓煌布先ド?Bytecode,會(huì)發(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 <SharedFunctionInfo add>)]  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í)行過(guò)程中的一些數(shù)據(jù)類型的分析信息都保存在這個(gè)反饋向量槽中了,目的是為了給 TurboFan 優(yōu)化編譯器提供優(yōu)化信息,很多字節(jié)碼都會(huì)為反饋向量槽提供運(yùn)行時(shí)信息。

  •  LdaSmi:將小整數(shù)(Smi)加載到累加器寄存器中

  •  Return:結(jié)束當(dāng)前函數(shù)的執(zhí)行,并將控制權(quán)傳回給調(diào)用方。返回的值是累加器中的值。

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

隱藏類和內(nèi)聯(lián)緩存

JavaScript 是一門動(dòng)態(tài)語(yǔ)言,其執(zhí)行效率要低于靜態(tài)語(yǔ)言,V8 為了提升 JavaScript 的執(zhí)行速度,借鑒了很多靜態(tài)語(yǔ)言的特性,比如實(shí)現(xiàn)了 JIT 機(jī)制,為了提升對(duì)象的屬性訪問(wèn)速度而引入了隱藏類,為了加速運(yùn)算而引入了內(nèi)聯(lián)緩存。

為什么靜態(tài)語(yǔ)言的效率更高?&emsp;&emsp;

靜態(tài)語(yǔ)言中,如 C++ 在聲明一個(gè)對(duì)象之前需要定義該對(duì)象的結(jié)構(gòu),代碼在執(zhí)行之前需要先被編譯,編譯的時(shí)候,每個(gè)對(duì)象的形狀都是固定的,也就是說(shuō),在代碼的執(zhí)行過(guò)程中是無(wú)法被改變的??梢灾苯油ㄟ^(guò)偏移量查詢來(lái)查詢對(duì)象的屬性值,這也就是靜態(tài)語(yǔ)言的執(zhí)行效率高的一個(gè)原因。&emsp;&emsp;

JavaScript 在運(yùn)行時(shí),對(duì)象的屬性是可以被修改的,所以當(dāng) V8 使用了一個(gè)對(duì)象時(shí),比如使用了 obj.x 的時(shí)候,它并不知道該對(duì)象中是否有 x,也不知道 x 相對(duì)于對(duì)象的偏移量是多少,也就是說(shuō) V8 并不知道該對(duì)象的具體的形狀。那么,當(dāng)在 JavaScript 中要查詢對(duì)象 obj 中的 x 屬性時(shí),V8 會(huì)按照具體的規(guī)則一步一步來(lái)查詢,這個(gè)過(guò)程非常的慢且耗時(shí)。

將靜態(tài)的特性引入到 V8

  •  V8 采用的一個(gè)思路就是將 JavaScript 中的對(duì)象靜態(tài)化,也就是 V8 在運(yùn)行 JavaScript 的過(guò)程中,會(huì)假設(shè) JavaScript 中的對(duì)象是靜態(tài)的。

  •  具體地講,V8 對(duì)每個(gè)對(duì)象做如下兩點(diǎn)假設(shè):

    •  對(duì)象創(chuàng)建好了之后就不會(huì)添加新的屬性;

    •   對(duì)象創(chuàng)建好了之后也不會(huì)刪除屬性。

  •  符合這兩個(gè)假設(shè)之后,V8 就可以對(duì) JavaScript 中的對(duì)象做深度優(yōu)化了。V8 會(huì)為每個(gè)對(duì)象創(chuàng)建一個(gè)隱藏類,對(duì)象的隱藏類中記錄了該對(duì)象一些基礎(chǔ)的布局信息,包括以下兩點(diǎn):

    •  對(duì)象中所包含的所有的屬性;

    •  每個(gè)屬性相對(duì)于對(duì)象的偏移量。

  •  有了隱藏類之后,那么當(dāng) V8 訪問(wèn)某個(gè)對(duì)象中的某個(gè)屬性時(shí),就會(huì)先去隱藏類中查找該屬性相對(duì)于它的對(duì)象的偏移量,有了偏移量和屬性類型,V8 就可以直接去內(nèi)存中取出對(duì)應(yīng)的屬性值,而不需要經(jīng)歷一系列的查找過(guò)程,那么這就大大提升了 V8 查找對(duì)象的效率。

  •  在 V8 中,把隱藏類又稱為 map,每個(gè)對(duì)象都有一個(gè) map 屬性,其值指向內(nèi)存中的隱藏類;

  •  map 描述了對(duì)象的內(nèi)存布局,比如對(duì)象都包括了哪些屬性,這些數(shù)據(jù)對(duì)應(yīng)于對(duì)象的偏移量是多少。

通過(guò) 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 對(duì)象創(chuàng)建的隱藏類   - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]   - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>   - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]   - properties: 0x1ea3080406e9 <FixedArray[0]> {      #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 <Map(HOLEY_ELEMENTS)>   - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>   - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>   - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>   - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>   - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>   - construction counter: 0  # ===============  DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE]  # V8 為 point2 對(duì)象創(chuàng)建的隱藏類   - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]   - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>   - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]   - properties: 0x1ea3080406e9 <FixedArray[0]> {      #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 <Map(HOLEY_ELEMENTS)>   - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>   - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>   - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>   - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>   - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>   - construction counter: 0  # ===============  DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE]  # V8 為 point3 對(duì)象創(chuàng)建的隱藏類   - map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties]   - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>   - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]   - properties: 0x1ea3080406e9 <FixedArray[0]> {      #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 <Map(HOLEY_ELEMENTS)>   - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>   - instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]>   - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>   - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>   - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>   - construction counter: 0

多個(gè)對(duì)象共用一個(gè)隱藏類

  •  在 V8 中,每個(gè)對(duì)象都有一個(gè) map 屬性,該屬性值指向該對(duì)象的隱藏類。不過(guò)如果兩個(gè)對(duì)象的形狀是相同的,V8 就會(huì)為其復(fù)用同一個(gè)隱藏類,這樣有兩個(gè)好處:

    •  減少隱藏類的創(chuàng)建次數(shù),也間接加速了代碼的執(zhí)行速度;

    •  減少了隱藏類的存儲(chǔ)空間。

  •  那么,什么情況下兩個(gè)對(duì)象的形狀是相同的,要滿足以下兩點(diǎn):

    •  相同的屬性名稱;

    •  相等的屬性個(gè)數(shù)。

重新構(gòu)建隱藏類

  •  給一個(gè)對(duì)象添加新的屬性,刪除新的屬性,或者改變某個(gè)屬性的數(shù)據(jù)類型都會(huì)改變這個(gè)對(duì)象的形狀,那么勢(shì)必也就會(huì)觸發(fā) V8 為改變形狀后的對(duì)象重建新的隱藏類。 

// 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 <Map(HOLEY_ELEMENTS)> [FastProperties]   ...  DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]   - map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties]   ...  DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]   - map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]   ...
  •  每次給對(duì)象添加了一個(gè)新屬性之后,該對(duì)象的隱藏類的地址都會(huì)改變,這也就意味著隱藏類也隨著改變了;如果刪除對(duì)象的某個(gè)屬性,那么對(duì)象的形狀也就隨著發(fā)生了改變,這時(shí) V8 也會(huì)重建該對(duì)象的隱藏類;

  •  最佳實(shí)踐

    • 使用字面量初始化對(duì)象時(shí),要保證屬性的順序是一致的;

    • 盡量使用字面量一次性初始化完整對(duì)象屬性;

    • 盡量避免使用 delete 方法。

通過(guò)內(nèi)聯(lián)緩存來(lái)提升函數(shù)執(zhí)行效率

雖然隱藏類能夠加速查找對(duì)象的速度,但是在 V8 查找對(duì)象屬性值的過(guò)程中,依然有查找對(duì)象的隱藏類和根據(jù)隱藏類來(lái)查找對(duì)象屬性值的過(guò)程。如果一個(gè)函數(shù)中利用了對(duì)象的屬性,并且這個(gè)函數(shù)會(huì)被多次執(zhí)行:

function loadX(obj) {    return obj.x;  }  var obj = { x: 1, y: 3 };  var obj1 = { x: 3, y: 6 };  var obj2 = { x: 3, y: 6, z: 8 };  for (var i = 0; i < 90000; i++) {    loadX(obj);    loadX(obj1);    // 產(chǎn)生多態(tài)    loadX(obj2);  }

通常 V8 獲取 obj.x 的流程:

  •  找對(duì)象 obj 的隱藏類;

  •  再通過(guò)隱藏類查找 x 屬性偏移量;

  •  然后根據(jù)偏移量獲取屬性值,在這段代碼中 loadX 函數(shù)會(huì)被反復(fù)執(zhí)行,那么獲取 obj.x 的流程也需要反復(fù)被執(zhí)行;

內(nèi)聯(lián)緩存及其原理:

  •  函數(shù) loadX 在一個(gè) for 循環(huán)里面被重復(fù)執(zhí)行了很多次,因此 V8 會(huì)想盡一切辦法來(lái)壓縮這個(gè)查找過(guò)程,以提升對(duì)象的查找效率。這個(gè)加速函數(shù)執(zhí)行的策略就是內(nèi)聯(lián)緩存 (Inline Cache),簡(jiǎn)稱為 IC;

  •  IC 的原理:在 V8 執(zhí)行函數(shù)的過(guò)程中,會(huì)觀察函數(shù)中一些調(diào)用點(diǎn) (CallSite) 上的關(guān)鍵中間數(shù)據(jù),然后將這些數(shù)據(jù)緩存起來(lái),當(dāng)下次再次執(zhí)行該函數(shù)的時(shí)候,V8 就可以直接利用這些中間數(shù)據(jù),節(jié)省了再次獲取這些數(shù)據(jù)的過(guò)程,因此 V8 利用 IC,可以有效提升一些重復(fù)代碼的執(zhí)行效率。

  •  IC 會(huì)為每個(gè)函數(shù)維護(hù)一個(gè)反饋向量 (FeedBack Vector),反饋向量記錄了函數(shù)在執(zhí)行過(guò)程中的一些關(guān)鍵的中間數(shù)據(jù)。

  •  反饋向量其實(shí)就是一個(gè)表結(jié)構(gòu),它由很多項(xiàng)組成的,每一項(xiàng)稱為一個(gè)插槽 (Slot),V8 會(huì)依次將執(zhí)行 loadX 函數(shù)的中間數(shù)據(jù)寫(xiě)入到反饋向量的插槽中。

  •  當(dāng) V8 再次調(diào)用 loadX 函數(shù)時(shí),比如執(zhí)行到 loadX 函數(shù)中的 return obj.x 語(yǔ)句時(shí),它就會(huì)在對(duì)應(yīng)的插槽中查找 x 屬性的偏移量,之后 V8 就能直接去內(nèi)存中獲取 obj.x 的屬性值了。這樣就大大提升了 V8 的執(zhí)行效率。

單態(tài)、多態(tài)和超態(tài):

  •  如果一個(gè)插槽中只包含 1 個(gè)隱藏類,那么我們稱這種狀態(tài)為單態(tài) (monomorphic);

  •  如果一個(gè)插槽中包含了 2 ~ 4 個(gè)隱藏類,那我們稱這種狀態(tài)為多態(tài) (polymorphic);

  •  如果一個(gè)插槽中超過(guò) 4 個(gè)隱藏類,那我們稱這種狀態(tài)為超態(tài) (magamorphic)。

  •  單態(tài)的性能優(yōu)于多態(tài)和超態(tài),所以我們需要稍微避免多態(tài)和超態(tài)的情況。要避免多態(tài)和超態(tài),那么就盡量默認(rèn)所有的對(duì)象屬性是不變的,比如你寫(xiě)了一個(gè) loadX(obj) 的函數(shù),那么當(dāng)傳遞參數(shù)時(shí),盡量不要使用多個(gè)不同形狀的 obj 對(duì)象。

總結(jié):

V8 引入了內(nèi)聯(lián)緩存(IC),IC 會(huì)監(jiān)聽(tīng)每個(gè)函數(shù)的執(zhí)行過(guò)程,并在一些關(guān)鍵的地方埋下監(jiān)聽(tīng)點(diǎn),這些包括了加載對(duì)象屬性 (Load)、給對(duì)象屬性賦值 (Store)、還有函數(shù)調(diào)用 (Call),V8 會(huì)將監(jiān)聽(tīng)到的數(shù)據(jù)寫(xiě)入一個(gè)稱為反饋向量 (FeedBack Vector) 的結(jié)構(gòu)中,同時(shí) V8 會(huì)為每個(gè)執(zhí)行的函數(shù)維護(hù)一個(gè)反饋向量。有了反饋向量緩存的臨時(shí)數(shù)據(jù),V8 就可以縮短對(duì)象屬性的查找路徑,從而提升執(zhí)行效率。但是針對(duì)函數(shù)中的同一段代碼,如果對(duì)象的隱藏類是不同的,那么反饋向量也會(huì)記錄這些不同的隱藏類,這就出現(xiàn)了多態(tài)和超態(tài)的情況。我們?cè)趯?shí)際項(xiàng)目中,要盡量避免出現(xiàn)多態(tài)或者超態(tài)的情況。

異步編程與消息隊(duì)列

V8 是如何執(zhí)行回調(diào)函數(shù)的

回調(diào)函數(shù)有兩種類型:同步回調(diào)和異步回調(diào),同步回調(diào)函數(shù)是在執(zhí)行函數(shù)內(nèi)部被執(zhí)行的,而異步回調(diào)函數(shù)是在執(zhí)行函數(shù)外部被執(zhí)行的。&emsp;&emsp;

通用 UI 線程宏觀架構(gòu):

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法&emsp;&emsp;

UI 線程提供一個(gè)消息隊(duì)列,并將待執(zhí)行的事件添加到消息隊(duì)列中,然后 UI 線程會(huì)不斷循環(huán)地從消息隊(duì)列中取出事件、執(zhí)行事件。關(guān)于異步回調(diào),這里也有兩種不同的類型,其典型代表是 setTimeout 和 XMLHttpRequest:

  •  setTimeout 的執(zhí)行流程其實(shí)是比較簡(jiǎn)單的,在 setTimeout 函數(shù)內(nèi)部封裝回調(diào)消息,并將回調(diào)消息添加進(jìn)消息隊(duì)列,然后主線程從消息隊(duì)列中取出回調(diào)事件,并執(zhí)行回調(diào)函數(shù)。

  •  XMLHttpRequest 稍微復(fù)雜一點(diǎn),因?yàn)橄螺d過(guò)程需要放到單獨(dú)的一個(gè)線程中去執(zhí)行,所以執(zhí)行 XMLHttpRequest.send 的時(shí)候,宿主會(huì)將實(shí)際請(qǐng)求轉(zhuǎn)發(fā)給網(wǎng)絡(luò)線程,然后 send 函數(shù)退出,主線程繼續(xù)執(zhí)行下面的任務(wù)。網(wǎng)絡(luò)線程在執(zhí)行下載的過(guò)程中,會(huì)將一些中間信息和回調(diào)函數(shù)封裝成新的消息,并將其添加進(jìn)消息隊(duì)列中,然后主線程從消息隊(duì)列中取出回調(diào)事件,并執(zhí)行回調(diào)函數(shù)。

宏任務(wù)和微任務(wù)

  •  調(diào)用棧:調(diào)用棧是一種數(shù)據(jù)結(jié)構(gòu),用來(lái)管理在主線程上執(zhí)行的函數(shù)的調(diào)用關(guān)系。主線程在執(zhí)行任務(wù)的過(guò)程中,如果函數(shù)的調(diào)用層次過(guò)深,可能造成棧溢出的錯(cuò)誤,我們可以使用 setTimeout 來(lái)解決棧溢出的問(wèn)題。setTimeout 的本質(zhì)是將同步函數(shù)調(diào)用改成異步函數(shù)調(diào)用,這里的異步調(diào)用是將回調(diào)函數(shù)封裝成宏任務(wù),并將其添加進(jìn)消息隊(duì)列中,然后主線程再按照一定規(guī)則循環(huán)地從消息隊(duì)列中讀取下一個(gè)宏任務(wù)。

  •  宏任務(wù):就是指消息隊(duì)列中的等待被主線程執(zhí)行的事件。每個(gè)宏任務(wù)在執(zhí)行時(shí),V8 都會(huì)重新創(chuàng)建棧,然后隨著宏任務(wù)中函數(shù)調(diào)用,棧也隨之變化,最終,當(dāng)該宏任務(wù)執(zhí)行結(jié)束時(shí),整個(gè)棧又會(huì)被清空,接著主線程繼續(xù)執(zhí)行下一個(gè)宏任務(wù)。

  •  微任務(wù):你可以把微任務(wù)看成是一個(gè)需要異步執(zhí)行的函數(shù),執(zhí)行時(shí)機(jī)是在主函數(shù)執(zhí)行結(jié)束之后、當(dāng)前宏任務(wù)結(jié)束之前。

  •  JavaScript 中之所以要引入微任務(wù),主要是由于主線程執(zhí)行消息隊(duì)列中宏任務(wù)的時(shí)間顆粒度太粗了,無(wú)法勝任一些對(duì)精度和實(shí)時(shí)性要求較高的場(chǎng)景,微任務(wù)可以在實(shí)時(shí)性和效率之間做一個(gè)有效的權(quán)衡。另外使用微任務(wù),可以改變我們現(xiàn)在的異步編程模型,使得我們可以使用同步形式的代碼來(lái)編寫(xiě)異步調(diào)用。

  • 微任務(wù)是基于消息隊(duì)列、事件循環(huán)、UI 主線程還有堆棧而來(lái)的,然后基于微任務(wù),又可以延伸出協(xié)程、Promise、Generator、await/async 等現(xiàn)代前端經(jīng)常使用的一些技術(shù)。

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

// 不會(huì)使瀏覽器卡死    function foo() {      setTimeout(foo, 0);    }    foo();

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

微任務(wù):

// 瀏覽器console控制臺(tái)可使瀏覽器卡死(無(wú)法響應(yīng)鼠標(biāo)事件等)  function foo() {    return Promise.resolve().then(foo);  }  foo();
  •  如果當(dāng)前的任務(wù)中產(chǎn)生了一個(gè)微任務(wù),通過(guò) Promise.resolve() 或者 Promise.reject() 都會(huì)觸發(fā)微任務(wù),觸發(fā)的微任務(wù)不會(huì)在當(dāng)前的函數(shù)中被執(zhí)行,所以*執(zhí)行微任務(wù)時(shí),不會(huì)導(dǎo)致棧的無(wú)限擴(kuò)張;

  •  和異步調(diào)用不同,微任務(wù)依然會(huì)在當(dāng)前任務(wù)執(zhí)行結(jié)束之前被執(zhí)行,這也就意味著在當(dāng)前微任務(wù)執(zhí)行結(jié)束之前,消息隊(duì)列中的其他任務(wù)是不可能被執(zhí)行的。因此在函數(shù)內(nèi)部觸發(fā)的微任務(wù),一定比在函數(shù)內(nèi)部觸發(fā)的宏任務(wù)要優(yōu)先執(zhí)行。

  •  微任務(wù)依然是在當(dāng)前的任務(wù)中執(zhí)行的,所以如果在微任務(wù)中循環(huán)觸發(fā)新的微任務(wù),那么將導(dǎo)致消息隊(duì)列中的其他任務(wù)沒(méi)有機(jī)會(huì)被執(zhí)行。

前端異步編程方案史

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

  •  Callback 模式的異步編程模型需要實(shí)現(xiàn)大量的回調(diào)函數(shù),大量的回調(diào)函數(shù)會(huì)打亂代碼的正常邏輯,使得代碼變得不線性、不易閱讀,這就是我們所說(shuō)的回調(diào)地獄問(wèn)題。

  •  Promise 能很好地解決回調(diào)地獄的問(wèn)題,我們可以按照線性的思路來(lái)編寫(xiě)代碼,這個(gè)過(guò)程是線性的,非常符合人的直覺(jué)。

  •  但是這種方式充滿了 Promise 的 then() 方法,如果處理流程比較復(fù)雜的話,那么整段代碼將充斥著大量的 then,語(yǔ)義化不明顯,代碼不能很好地表示執(zhí)行流程。我們想要通過(guò)線性的方式來(lái)編寫(xiě)異步代碼,要實(shí)現(xiàn)這個(gè)理想,最關(guān)鍵的是要能實(shí)現(xiàn)函數(shù)暫停和恢復(fù)執(zhí)行的功能。而生成器就可以實(shí)現(xiàn)函數(shù)暫停和恢復(fù),我們可以在生成器中使用同步代碼的邏輯來(lái)異步代碼 (實(shí)現(xiàn)該邏輯的核心是協(xié)程)。

  •  但是在生成器之外,我們還需要一個(gè)觸發(fā)器來(lái)驅(qū)動(dòng)生成器的執(zhí)行。前端的最終方案就是 async/await,async 是一個(gè)可以暫停和恢復(fù)執(zhí)行的函數(shù),在 async 函數(shù)內(nèi)部使用 await 來(lái)暫停 async 函數(shù)的執(zhí)行,await 等待的是一個(gè) Promise 對(duì)象,如果 Promise 的狀態(tài)變成 resolve 或者 reject,那么 async 函數(shù)會(huì)恢復(fù)執(zhí)行。因此,使用 async/await 可以實(shí)現(xiàn)以同步的方式編寫(xiě)異步代碼這一目標(biāo)。和生成器函數(shù)一樣,使用了 async 聲明的函數(shù)在執(zhí)行時(shí),也是一個(gè)單獨(dú)的協(xié)程,我們可以使用 await 來(lái)暫停該協(xié)程,由于 await 等待的是一個(gè) Promise 對(duì)象,我們可以 resolve 來(lái)恢復(fù)該協(xié)程。

協(xié)程是一種比線程更加輕量級(jí)的存在。你可以把協(xié)程看成是跑在線程上的任務(wù),一個(gè)線程上可以存在多個(gè)協(xié)程,但是在線程上同時(shí)只能執(zhí)行一個(gè)協(xié)程。比如,當(dāng)前執(zhí)行的是 A 協(xié)程,要啟動(dòng) B 協(xié)程,那么 A 協(xié)程就需要將主線程的控制權(quán)交給 B 協(xié)程,這就體現(xiàn)在 A 協(xié)程暫停執(zhí)行,B 協(xié)程恢復(fù)執(zhí)行;同樣,也可以從 B 協(xié)程中啟動(dòng) A 協(xié)程。通常,如果從 A 協(xié)程啟動(dòng) B 協(xié)程,我們就把 A 協(xié)程稱為 B 協(xié)程的父協(xié)程。

正如一個(gè)進(jìn)程可以擁有多個(gè)線程一樣,一個(gè)線程也可以擁有多個(gè)協(xié)程。每一時(shí)刻,該線程只能執(zhí)行其中某一個(gè)協(xié)程。最重要的是,協(xié)程不是被操作系統(tǒng)內(nèi)核所管理,而完全是由程序所控制(也就是在用戶態(tài)執(zhí)行)。這樣帶來(lái)的好處就是性能得到了很大的提升,不會(huì)像線程切換那樣消耗資源。

垃圾回收

垃圾數(shù)據(jù)&emsp;&emsp;

從“GC Roots”對(duì)象出發(fā),遍歷 GC Root 中的所有對(duì)象,如果通過(guò) GC Roots 沒(méi)有遍歷到的對(duì)象,則這些對(duì)象便是垃圾數(shù)據(jù)。V8 會(huì)有專門的垃圾回收器來(lái)回收這些垃圾數(shù)據(jù)。

垃圾回收算法

垃圾回收大致可以分為以下幾個(gè)步驟:

  •  第一步,通過(guò) GC Root 標(biāo)記空間中活動(dòng)對(duì)象和非活動(dòng)對(duì)象。目前 V8 采用的可訪問(wèn)性(reachability)算法來(lái)判斷堆中的對(duì)象是否是活動(dòng)對(duì)象。具體地講,這個(gè)算法是將一些 GC Root 作為初始存活的對(duì)象的集合,從 GC Roots 對(duì)象出發(fā),遍歷 GC Root 中的所有對(duì)象:

    • 全局的 window 對(duì)象(位于每個(gè) iframe 中);

    • 文檔 DOM 樹(shù),由可以通過(guò)遍歷文檔到達(dá)的所有原生 DOM 節(jié)點(diǎn)組成;

    • 存放棧上變量。

    • 通過(guò) GC Root 遍歷到的對(duì)象,我們就認(rèn)為該對(duì)象是可訪問(wèn)的(reachable),那么必須保證這些對(duì)象應(yīng)該在內(nèi)存中保留,我們也稱可訪問(wèn)的對(duì)象為活動(dòng)對(duì)象;

    • 通過(guò) GC Roots 沒(méi)有遍歷到的對(duì)象,則是不可訪問(wèn)的(unreachable),那么這些不可訪問(wèn)的對(duì)象就可能被回收,我們稱不可訪問(wèn)的對(duì)象為非活動(dòng)對(duì)象。

    • 在瀏覽器環(huán)境中,GC Root 有很多,通常包括了以下幾種 (但是不止于這幾種):

  •  第二步,回收非活動(dòng)對(duì)象所占據(jù)的內(nèi)存。其實(shí)就是在所有的標(biāo)記完成之后,統(tǒng)一清理內(nèi)存中所有被標(biāo)記為可回收的對(duì)象。

  •  第三步,做內(nèi)存整理。一般來(lái)說(shuō),頻繁回收對(duì)象后,內(nèi)存中就會(huì)存在大量不連續(xù)空間,我們把這些不連續(xù)的內(nèi)存空間稱為內(nèi)存碎片。當(dāng)內(nèi)存中出現(xiàn)了大量的內(nèi)存碎片之后,如果需要分配較大的連續(xù)內(nèi)存時(shí),就有可能出現(xiàn)內(nèi)存不足的情況,所以最后一步需要整理這些內(nèi)存碎片。但這步其實(shí)是可選的,因?yàn)橛械睦厥掌鞑粫?huì)產(chǎn)生內(nèi)存碎片(比如副垃圾回收器)。

垃圾回收

  •  V8 依據(jù)代際假說(shuō),將堆內(nèi)存劃分為新生代和老生代兩個(gè)區(qū)域,新生代中存放的是生存時(shí)間短的對(duì)象,老生代中存放生存時(shí)間久的對(duì)象。代際假說(shuō)有兩個(gè)特點(diǎn):

    •  第一個(gè)是大部分對(duì)象都是“朝生夕死”的,也就是說(shuō)大部分對(duì)象在內(nèi)存中存活的時(shí)間很短,比如函數(shù)內(nèi)部聲明的變量,或者塊級(jí)作用域中的變量,當(dāng)函數(shù)或者代碼塊執(zhí)行結(jié)束時(shí),作用域中定義的變量就會(huì)被銷毀。因此這一類對(duì)象一經(jīng)分配內(nèi)存,很快就變得不可訪問(wèn);

    •  第二個(gè)是不死的對(duì)象,會(huì)活得更久,比如全局的 window、DOM、Web API 等對(duì)象。

  •  為了提升垃圾回收的效率,V8 設(shè)置了兩個(gè)垃圾回收器,主垃圾回收器和副垃圾回收器。

    • 主垃圾回收器主要負(fù)責(zé)老生代中的垃圾回收。除了新生代中晉升的對(duì)象,一些大的對(duì)象會(huì)直接被分配到老生代里。

    • 老生代中的對(duì)象有兩個(gè)特點(diǎn):一個(gè)是對(duì)象占用空間大;另一個(gè)是對(duì)象存活時(shí)間長(zhǎng)。

    • 這種角色翻轉(zhuǎn)的操作還能讓新生代中的這兩塊區(qū)域無(wú)限重復(fù)使用下去。

    • 副垃圾回收器每次執(zhí)行清理操作時(shí),都需要將存活的對(duì)象從對(duì)象區(qū)域復(fù)制到空閑區(qū)域,復(fù)制操作需要時(shí)間成本,如果新生區(qū)空間設(shè)置得太大了,那么每次清理的時(shí)間就會(huì)過(guò)久,所以為了執(zhí)行效率,一般新生區(qū)的空間會(huì)被設(shè)置得比較小。

    • 副垃圾回收器還會(huì)采用對(duì)象晉升策略,也就是移動(dòng)那些經(jīng)過(guò)兩次垃圾回收依然還存活的對(duì)象到老生代中。

    • 主垃圾回收器負(fù)責(zé)收集老生代中的垃圾數(shù)據(jù),副垃圾回收器負(fù)責(zé)收集新生代中的垃圾數(shù)據(jù)。

    • 副垃圾回收器采用了 Scavenge 算法,是把新生代空間對(duì)半劃分為兩個(gè)區(qū)域(有些地方也稱作From和To空間),一半是對(duì)象區(qū)域,一半是空閑區(qū)域。新的數(shù)據(jù)都分配在對(duì)象區(qū)域,等待對(duì)象區(qū)域快分配滿的時(shí)候,垃圾回收器便執(zhí)行垃圾回收操作,之后將存活的對(duì)象從對(duì)象區(qū)域拷貝到空閑區(qū)域,并將兩個(gè)區(qū)域互換。

    • 主垃圾回收器回收器主要負(fù)責(zé)老生代中的垃圾數(shù)據(jù)的回收操作,會(huì)經(jīng)歷標(biāo)記、清除和整理過(guò)程。

Stop-The-World

由于 JavaScript 是運(yùn)行在主線程之上的,因此,一旦執(zhí)行垃圾回收算法,都需要將正在執(zhí)行的 JavaScript 腳本暫停下來(lái),待垃圾回收完畢后再恢復(fù)腳本執(zhí)行。我們把這種行為叫做全停頓(Stop-The-World)。

  •  V8 最開(kāi)始的垃圾回收器有兩個(gè)特點(diǎn):

    • 第一個(gè)是垃圾回收在主線程上執(zhí)行,

    • 第二個(gè)特點(diǎn)是一次執(zhí)行一個(gè)完整的垃圾回收流程。

  •  由于這兩個(gè)原因,很容易造成主線程卡頓,所以 V8 采用了很多優(yōu)化執(zhí)行效率的方案。

    • 第一個(gè)方案是并行回收,在執(zhí)行一個(gè)完整的垃圾回收過(guò)程中,垃圾回收器會(huì)使用多個(gè)輔助線程來(lái)并行執(zhí)行垃圾回收。

    • 第二個(gè)方案是增量式垃圾回收,垃圾回收器將標(biāo)記工作分解為更小的塊,并且穿插在主線程不同的任務(wù)之間執(zhí)行。采用增量垃圾回收時(shí),垃圾回收器沒(méi)有必要一次執(zhí)行完整的垃圾回收過(guò)程,每次執(zhí)行的只是整個(gè)垃圾回收過(guò)程中的一小部分工作。

    • 第三個(gè)方案是并發(fā)回收,回收線程在執(zhí)行 JavaScript 的過(guò)程,輔助線程能夠在后臺(tái)完成的執(zhí)行垃圾回收的操作。

    • 主垃圾回收器就綜合采用了所有的方案(并發(fā)標(biāo)記,增量標(biāo)記,輔助清理),副垃圾回收器也采用了部分方案。

似此星辰非昨夜,為誰(shuí)風(fēng)露立中宵

Breaking the JavaScript Speed Limit with V8&emsp;&emsp;

Daniel Clifford 在 Google I/O 2012 上做了一個(gè)精彩的演講“Breaking the JavaScript Speed Limit with V8”。在演講中,他深入解釋了 13 個(gè)簡(jiǎn)單的代碼優(yōu)化方法,可以讓你的JavaScript代碼在 Chrome V8 引擎編譯/運(yùn)行時(shí)更加快速。在演講中,他介紹了怎么優(yōu)化,并解釋了原因。下面簡(jiǎn)明的列出了13 個(gè) JavaScript 性能提升技巧:

  1. 鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)

  2.  在構(gòu)造函數(shù)里初始化所有對(duì)象的成員(所以這些實(shí)例之后不會(huì)改變其隱藏類);

  3.  總是以相同的次序初始化對(duì)象成員;

  4.  盡量使用可以用 31 位有符號(hào)整數(shù)表示的數(shù);

  5.  為數(shù)組使用從 0 開(kāi)始的連續(xù)的主鍵;

  6.  別預(yù)分配大數(shù)組(比如大于 64K 個(gè)元素)到其最大尺寸,令其尺寸順其自然發(fā)展就好;

  7.  別刪除數(shù)組里的元素,尤其是數(shù)字?jǐn)?shù)組;

  8.  別加載未初始化或已刪除的元素;

  9.  對(duì)于固定大小的數(shù)組,使用”array literals“初始化(初始化小額定長(zhǎng)數(shù)組時(shí),用字面量進(jìn)行初始化);

  10.  小數(shù)組(小于 64k)在使用之前先預(yù)分配正確的尺寸;

  11.  請(qǐng)勿在數(shù)字?jǐn)?shù)組中存放非數(shù)字的值(對(duì)象);

  12.  盡量使用單一類型(monomorphic)而不是多類型(polymorphic)(如果通過(guò)非字面量進(jìn)行初始化小數(shù)組時(shí),切勿觸發(fā)類型的重新轉(zhuǎn)換);

  13.  不要使用 try{} catch{}(如果存在 try/catch 代碼快,則將性能敏感的代碼放到一個(gè)嵌套的函數(shù)中);

  14.  在優(yōu)化后避免在方法中修改隱藏類。

在 V8 引擎里 5 個(gè)優(yōu)化代碼的技巧

  1.  對(duì)象屬性的順序: 在實(shí)例化你的對(duì)象屬性的時(shí)候一定要使用相同的順序,這樣隱藏類和隨后的優(yōu)化代碼才能共享;

  2.  動(dòng)態(tài)屬性: 在對(duì)象實(shí)例化之后再添加屬性會(huì)強(qiáng)制使得隱藏類變化,并且會(huì)減慢為舊隱藏類所優(yōu)化的代碼的執(zhí)行。所以,要在對(duì)象的構(gòu)造函數(shù)中完成所有屬性的分配;

  3.  方法: 重復(fù)執(zhí)行相同的方法會(huì)運(yùn)行的比不同的方法只執(zhí)行一次要快 (因?yàn)閮?nèi)聯(lián)緩存);

  4.  數(shù)組: 避免使用 keys 不是遞增的數(shù)字的稀疏數(shù)組,這種 key 值不是遞增數(shù)字的稀疏數(shù)組其實(shí)是一個(gè) hash 表。在這種數(shù)組中每一個(gè)元素的獲取都是昂貴的代價(jià)。同時(shí),要避免提前申請(qǐng)大數(shù)組。最好的做法是隨著你的需要慢慢的增大數(shù)組。最后,不要?jiǎng)h除數(shù)組中的元素,因?yàn)檫@會(huì)使得 keys 變得稀疏;

  5.  標(biāo)記值 (Tagged values): V8 用 32 位來(lái)表示對(duì)象和數(shù)字。它使用一位來(lái)區(qū)分它是對(duì)象 (flag = 1) 還是一個(gè)整型 (flag = 0),也被叫做小整型(SMI),因?yàn)樗挥?31 位。然后,如果一個(gè)數(shù)值大于 31 位,V8 將會(huì)對(duì)其進(jìn)行 box 操作,然后將其轉(zhuǎn)換成 double 型,并且創(chuàng)建一個(gè)新的對(duì)象來(lái)裝這個(gè)數(shù)。所以,為了避免代價(jià)很高的 box 操作,盡量使用 31 位的有符號(hào)數(shù)。

JavaScript 啟動(dòng)性能瓶頸分析與解決方案

資料參考:

  •  JavaScript Start-up Performance :

https://medium.com/reloading/javascript-start-up-performance-69200f43b201

  •  JavaScript 啟動(dòng)性能瓶頸分:析與解決方案:

https://zhuanlan.zhihu.com/p/25221314

抽絲剝繭有窮時(shí),V8 綿綿無(wú)絕期

  •  v8官方文檔(https://v8.dev/)

  •  圖解 Google V8(https://time.geekbang.org/column/intro/296)

  •  瀏覽器工作原理與實(shí)踐(https://time.geekbang.org/column/intro/216)

  •  [[譯] JavaScript 如何工作:對(duì)引擎、運(yùn)行時(shí)、調(diào)用堆棧的概述]:https://juejin.im/post/6844903510538993671)

  •  [[譯] JavaScript 如何工作的: 事件循環(huán)和異步編程的崛起 + 5 個(gè)關(guān)于如何使用 async/await 編寫(xiě)更好的技巧](https://juejin.im/post/6844903518319411207)

番外篇

  •  Console Importer:Easily import JS and CSS resources from Chrome console. (可以在瀏覽器控制臺(tái)安裝 loadsh、moment、jQuery 等庫(kù),在控制臺(tái)直接驗(yàn)證、使用這些庫(kù)。)

  效果圖:

由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法

看完上述內(nèi)容,你們掌握由Google開(kāi)發(fā)的開(kāi)源JavaScript引擎V8的介紹以及使用方法的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI