瀏覽器是如何工做的:Chrome V8讓你更懂JavaScript

Chrome V8
  V8 是由 Google 開發的開源 JavaScript 引擎,也被稱爲虛擬機,模擬實際計算機各類功能來實現代碼的編譯和執行javascript

上圖清晰版

記得那年花下,深夜,初識謝娘時

爲何須要 JavaScript 引擎

  咱們寫的 JavaScript 代碼直接交給瀏覽器或者 Node 執行時,底層的 CPU 是不認識的,也無法執行。CPU 只認識本身的指令集,指令集對應的是彙編代碼。寫彙編代碼是一件很痛苦的事情。而且不一樣類型的 CPU 的指令集是不同的,那就意味着須要給每一種 CPU 重寫彙編代碼
  JavaScirpt 引擎能夠將 JS 代碼編譯爲不一樣 CPU(Intel, ARM 以及 MIPS 等)對應的彙編代碼,這樣咱們就不須要去翻閱每一個 CPU 的指令集手冊來編寫彙編代碼了。固然,JavaScript 引擎的工做也不僅是編譯代碼,它還要負責執行代碼、分配內存以及垃圾回收html

1000100111011000  #機器指令
mov ax,bx         #彙編指令
資料拓展: 彙編語言入門教程【阮一峯】 | 理解 V8 的字節碼「譯」

熱門 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、QuickJSHermes

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 能夠訪問宿主環境中的對象,並在腳本中完成對宿主對象的操做html5

how-v8-works

資料拓展: v8 logo | V8 (JavaScript engine) | 《V八、JavaScript+的如今與將來》 | 幾張圖讓你看懂 WebAssembly

與君初相識,猶如故人歸

什麼是 D8

  d8 是一個很是有用的調試工具,你能夠把它當作是 debug for V8 的縮寫。咱們可使用 d8 來查看 V8 在執行 JavaScript 過程當中的各類中間數據,好比做用域、AST、字節碼、優化的二進制代碼、垃圾回收的狀態,還可使用 d8 提供的私有 API 查看一些內部信息java

安裝 D8

本文後續用於 demo 演示時的文件目錄結構:c++

 V8:
    # d8可執行文件
    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
  • 方法三:macgit

    # 若是已有HomeBrew,忽略第一條命令
      ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
      brew install v8
  • 方法四:使用 node 代替,好比能夠用node --print-bytecode ./test.js,打印出 Ignition(解釋器)生成的 Bytecode(字節碼)。

都有哪些 d8 命令可供使用?

  • 查看 d8 命令程序員

    # 若是不想使用./d8這種方式進行調試,可將d8加入環境變量,以後就能夠直接`d8 --help`了
      ./d8 --help
  • 過濾特定的命令

    # 若是是 Windows 系統,可能缺乏 grep 程序,請自行下載安裝並添加環境變量
      ./d8 --help |grep print

    如:

    • print-bytecode 查看生成的字節碼
    • print-opt-code 查看優化後的代碼
    • print-ast 查看中間生成的 AST
    • print-scopes 查看中間生成的做用域
    • trace-gc 查看這段代碼的內存回收狀態
    • trace-opt 查看哪些代碼被優化了
    • trace-deopt 查看哪些代碼被反優化了
    • turbofan-stats 打印優化編譯器的一些統計數據

使用 d8 進行調試

// test.js
function sum(a) {
  var b = 6;
  return a + 6;
}
console.log(sum(3));
# d8 後面跟上文件名和要執行的命令,如執行下面這行命令,就會打印出 test.js 文件所生成的字節碼。
  ./d8 ./test.js --print-bytecode
  # 執行如下命令,輸出9
  ./d8 ./test.js

內部方法

  你還可使用 V8 所提供的一些內部方法,只須要在啓動 V8 時傳入 --allow-natives-syntax 命令,你就能夠在 test.js 中使用諸如HasFastProperties(檢查一個對象是否擁有快屬性)的內部方法(索引屬性、常規屬性、快屬性等下文會介紹)。

function Foo(property_num, element_num) {
  //添加可索引屬性
  for (let i = 0; i < element_num; i++) {
    this[i] = `element${i}`;
  }
  //添加常規屬性
  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
  # 依次打印:true false

心似雙絲網,中有千千結

V8 引擎的內部結構

  V8 是一個很是複雜的項目,有超過 100 萬行 C++代碼。它由許多子模塊構成,其中這 4 個模塊是最重要的:

  • Parser:負責將 JavaScript 源碼轉換爲 Abstract Syntax Tree (AST)
  • Ignition:interpreter,即解釋器,負責將 AST 轉換爲 Bytecode,解釋執行 Bytecode;同時收集 TurboFan 優化編譯所需的信息,好比函數參數的類型;解釋器執行時主要有四個模塊,內存中的字節碼、寄存器、棧、堆。

    一般有兩種類型的解釋器,基於棧 (Stack-based)和基於寄存器 (Register-based),基於棧的解釋器使用棧來保存函數參數、中間運算結果、變量等;基於寄存器的虛擬機則支持寄存器的指令操做,使用寄存器來保存參數、中間計算結果。一般,基於棧的虛擬機也定義了少許的寄存器,基於寄存器的虛擬機也有堆棧,其 區別體如今它們提供的指令集體系大多數解釋器都是基於棧的,好比 Java 虛擬機,.Net 虛擬機,還有早期的 V8 虛擬機。基於堆棧的虛擬機在處理函數調用、解決遞歸問題和切換上下文時簡單明快。而 如今的 V8 虛擬機則採用了基於寄存器的設計,它將一些中間數據保存到寄存器中。
    基於寄存器的解釋器架構基於寄存器的解釋器架構
    資料參考: 解釋器是如何解釋執行字節碼的?
  • TurboFan:compiler,即編譯器,利用 Ignitio 所收集的類型信息,將 Bytecode 轉換爲優化的彙編代碼;
  • Orinoco:garbage collector,垃圾回收模塊,負責將程序再也不須要的內存空間回收。

  其中,Parser,Ignition 以及 TurboFan 能夠將 JS 源碼編譯爲彙編代碼,其流程圖以下:
V8流程
  簡單地說,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 代碼的

  • V8 出現以前,全部的 JavaScript 虛擬機所採用的都是解釋執行的方式,這是 JavaScript 執行速度過慢的一個主要緣由。而 V8 率先引入了即時編譯(JIT)雙輪驅動的設計(混合使用編譯器和解釋器的技術),這是一種權衡策略,混合編譯執行和解釋執行這兩種手段,給 JavaScript 的執行速度帶來了極大的提高。V8 出現以後,各大廠商也都在本身的 JavaScript 虛擬機中引入了 JIT 機制,因此目前市面上 JavaScript 虛擬機都有着相似的架構。另外,V8 也是早於其餘虛擬機引入了惰性編譯、內聯緩存、隱藏類等機制,進一步優化了 JavaScript 代碼的編譯執行效率
  • V8 執行一段 JavaScript 的流程圖:
    V8執行一段JavaScript流程圖

    資料拓展: V8 是如何執行一段 JavaScript 代碼的?
  • V8 本質上是一個虛擬機,由於計算機只能識別二進制指令,因此要讓計算機執行一段高級語言一般有兩種手段:

    • 第一種是將高級代碼轉換爲二進制代碼,再讓計算機去執行;
    • 另一種方式是在計算機安裝一個解釋器,並由解釋器來解釋執行。
  • 解釋執行和編譯執行都有各自的優缺點,解釋執行啓動速度快,可是執行時速度慢,而編譯執行啓動速度慢,可是執行速度快。爲了充分地利用解釋執行和編譯執行的優勢,規避其缺點,V8 採用了一種權衡策略,在啓動過程當中採用瞭解釋執行的策略,可是若是某段代碼的執行頻率超過一個值,那麼 V8 就會採用優化編譯器將其編譯成執行效率更加高效的機器代碼
  • 總結:

    V8 執行一段 JavaScript 代碼所經歷的主要流程包括:

    • 初始化基礎環境;
    • 解析源碼生成 AST 和做用域;
    • 依據 AST 和做用域生成字節碼;
    • 解釋執行字節碼;
    • 監聽熱點代碼;
    • 優化熱點代碼爲二進制的機器代碼;
    • 反優化生成的二進制機器代碼。

一等公民與閉包

一等公民的定義

  • 在編程語言中,一等公民能夠做爲函數參數,能夠做爲函數返回值,也能夠賦值給變量。
  • 若是某個編程語言的函數,能夠和這個語言的數據類型作同樣的事情,咱們就把這個語言中的函數稱爲一等公民。例如,字符串在幾乎全部編程語言中都是一等公民,字符串能夠作爲函數參數,字符串能夠做爲函數返回值,字符串也能夠賦值給變量。對於各類編程語言來講,函數就不必定是一等公民了,好比 Java 8 以前的版本。
  • 對於 JavaScript 來講,函數能夠賦值給變量,也能夠做爲函數參數,還能夠做爲函數返回值,所以 JavaScript 中函數是一等公民

動態做用域與靜態做用域

  • 若是一門語言的做用域是靜態做用域,那麼符號之間的引用關係可以根據程序代碼在編譯時就肯定清楚,在運行時不會變。某個函數是在哪聲明的,就具備它所在位置的做用域。它可以訪問哪些變量,那麼就跟這些變量綁定了,在運行時就一直能訪問這些變量。即靜態做用域能夠由程序代碼決定,在編譯時就能徹底肯定。大多數語言都是靜態做用域的。
  • 動態做用域(Dynamic Scope)。也就是說,變量引用跟變量聲明不是在編譯時就綁定死了的。在運行時,它是在運行環境中動態地找一個相同名稱的變量。在 macOS 或 Linux 中用的 bash 腳本語言,就是動態做用域的。

閉包的三個基礎特性

  • JavaScript 語言容許在函數內部定義新的函數
  • 能夠在內部函數中訪問父函數中定義的變量
  • 由於 JavaScript 中的函數是一等公民,因此函數能夠做爲另一個函數的返回值
// 閉包(靜態做用域,一等公民,調用棧的矛盾體)
function foo() {
  var d = 20;
  return function inner(a, b) {
    const c = a + b + d;
    return c;
  };
}
const f = foo();

  關於閉包,可參考我之前的一篇文章,在此再也不贅述,在此主要談下閉包給 Chrome V8 帶來的問題及其解決策略。

惰性解析

  所謂惰性解析是指解析器在解析的過程當中,若是遇到函數聲明,那麼會跳過函數內部的代碼,並不會爲其生成 AST 和字節碼,而僅僅生成頂層代碼的 AST 和字節碼。

  • 在編譯 JavaScript 代碼的過程當中,V8 並不會一次性將全部的 JavaScript 解析爲中間代碼,這主要是基於如下兩點:

    • 首先,若是一次解析和編譯全部的 JavaScript 代碼,過多的代碼會增長編譯時間,這會嚴重影響到首次執行 JavaScript 代碼的速度,讓用戶感受到卡頓。由於有時候一個頁面的 JavaScript 代碼很大,若是要將全部的代碼一次性解析編譯完成,那麼會大大增長用戶的等待時間;
    • 其次,解析完成的字節碼和編譯以後的機器代碼都會存放在內存中,若是一次性解析和編譯全部 JavaScript 代碼,那麼這些中間代碼和機器代碼將會一直佔用內存
  • 基於以上的緣由,全部主流的 JavaScript 虛擬機都實現了惰性解析。
  • 閉包給惰性解析帶來的問題:上文的 d 不能隨着 foo 函數的執行上下文被銷燬掉。

預解析器

  V8 引入預解析器,好比當解析頂層代碼的時候,遇到了一個函數,那麼預解析器並不會直接跳過該函數,而是對該函數作一次快速的預解析。

  • 判斷當前函數是否是存在一些語法上的錯誤,發現了語法錯誤,那麼就會向 V8 拋出語法錯誤;
  • 檢查函數內部是否引用了外部變量,若是引用了外部的變量,預解析器會將棧中的變量複製到堆中,在下次執行到該函數的時候,直接使用堆中的引用,這樣就解決了閉包所帶來的問題

V8 內部是如何存儲對象的:快屬性和慢屬性

下面的代碼會輸出什麼:

// 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 規範中定義了數字屬性應該按照索引值大小升序排列,字符串屬性根據建立時的順序升序排列。在這裏咱們把對象中的數字屬性稱爲排序屬性,在 V8 中被稱爲 elements,字符串屬性就被稱爲常規屬性,在 V8 中被稱爲 properties。在 V8 內部,爲了有效地提高存儲和訪問這兩種屬性的性能,分別使用了兩個線性數據結構來分別保存排序屬性和常規屬性。同時 v8 將部分常規屬性直接存儲到對象自己,咱們把這稱爲對象內屬性 (in-object properties),不過對象內屬性的數量是固定的,默認是 10 個。

function Foo(property_num, element_num) {
  //添加可索引屬性
  for (let i = 0; i < element_num; i++) {
    this[i] = `element${i}`;
  }
  //添加常規屬性
  for (let i = 0; i < property_num; i++) {
    let ppt = `property${i}`;
    this[ppt] = ppt;
  }
}
var bar = new Foo(10, 10);

  能夠經過 Chrome 開發者工具的 Memory 標籤,捕獲查看當前的內存快照。經過增大第一個參數來查看存儲變化。

  咱們將保存在線性數據結構中的屬性稱之爲「快屬性」,由於線性數據結構中只須要經過索引便可以訪問到屬性,雖然訪問線性結構的速度快,可是若是從線性結構中添加或者刪除大量的屬性時,則執行效率會很是低,這主要由於會產生大量時間和內存開銷。所以,若是一個對象的屬性過多時,V8 就會採起另一種存儲策略,那就是「慢屬性」策略,但慢屬性的對象內部會有獨立的非線性數據結構 (字典) 做爲屬性存儲容器。全部的屬性元信息再也不是線性存儲的,而是直接保存在屬性字典中。

v8 屬性存儲:
v8屬性存儲

總結:

  由於 JavaScript 中的對象是由一組組屬性和值組成的,因此最簡單的方式是使用一個字典來保存屬性和值,可是因爲字典是非線性結構,因此若是使用字典,讀取效率會大大下降。爲了提高查找效率,V8 在對象中添加了兩個隱藏屬性,排序屬性和常規屬性,element 屬性指向了 elements 對象,在 elements 對象中,會按照順序存放排序屬性。properties 屬性則指向了 properties 對象,在 properties 對象中,會按照建立時的順序保存常規屬性。

  經過引入這兩個屬性,加速了 V8 查找屬性的速度,爲了更加進一步提高查找效率,V8 還實現了內置內屬性的策略,當常規屬性少於必定數量時,V8 就會將這些常規屬性直接寫進對象中,這樣又節省了一箇中間步驟。

  可是若是對象中的屬性過多時,或者存在反覆添加或者刪除屬性的操做,那麼 V8 就會將線性的存儲模式降級爲非線性的字典存儲模式,這樣雖然下降了查找速度,可是卻提高了修改對象的屬性的速度

資料拓展: 快屬性和慢屬性:V8 是怎樣提高對象屬性訪問速度的?

堆空間和棧空間

棧空間

  • 現代語言都是基於函數的,每一個函數在執行過程當中,都有本身的生命週期和做用域,當函數執行結束時,其做用域也會被銷燬,所以,咱們會使用棧這種數據結構來管理函數的調用過程,咱們也把管理函數調用過程的棧結構稱之爲調用棧
  • 棧空間主要是用來管理 JavaScript 函數調用的,棧是內存中連續的一塊空間,同時棧結構是「先進後出」的策略。在函數調用過程當中,涉及到上下文相關的內容都會存放在棧上,好比原生類型、引用到的對象的地址、函數的執行狀態、this 值等都會存在在棧上。當一個函數執行結束,那麼該函數的執行上下文便會被銷燬掉。
  • 棧空間的最大的特色是空間連續,因此在棧中每一個元素的地址都是固定的,所以棧空間的查找效率很是高,可是一般在內存中,很難分配到一塊很大的連續空間,所以,V8 對棧空間的大小作了限制,若是函數調用層過深,那麼 V8 就有可能拋出棧溢出的錯誤。
  • 棧的優點和缺點:

    • 棧的結構很是適合函數調用過程。
    • 在棧上分配資源和銷燬資源的速度很是快,這主要歸結於棧空間是連續的,分配空間和銷燬空間只須要移動下指針就能夠了。
    • 雖然操做速度很是快,可是棧也是有缺點的,其中最大的缺點也是它的優勢所形成的,那就是棧是連續的,因此要想在內存中分配一塊連續的大空間是很是難的,所以棧空間是有限的

      // 棧溢出
      function factorial(n) {
        if (n === 1) {
          return 1;
        }
        return n * factorial(n - 1);
      }
      console.log(factorial(50000));

堆空間

  • 堆空間是一種樹形的存儲結構,用來存儲對象類型的離散的數據,JavaScript 中除了原生類型的數據,其餘的都是對象類型,諸如函數、數組,在瀏覽器中還有 window 對象、document 對象等,這些都是存在堆空間的。
  • 宿主在啓動 V8 的過程當中,會同時建立堆空間和棧空間,再繼續往下執行,產生的新數據都會存放在這兩個空間中。

繼承

  繼承就是一個對象能夠訪問另一個對象中的屬性和方法,在 JavaScript 中,咱們經過原型和原型鏈的方式來實現了繼承特性

  JavaScript 的每一個對象都包含了一個隱藏屬性 __proto__ ,咱們就把該隱藏屬性 __proto__ 稱之爲該對象的原型 (prototype),__proto__ 指向了內存中的另一個對象,咱們就把 __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__實現繼承:

dog.__proto__ = animal;
dog.getInfo();

  一般隱藏屬性是不能使用 JavaScript 來直接與之交互的。雖然現代瀏覽器都開了一個口子,讓 JavaScript 能夠訪問隱藏屬性 __proto__,可是在實際項目中,咱們不該該直接經過 __proto__ 來訪問或者修改該屬性,其主要緣由有兩個:

  • 首先,這是隱藏屬性,並非標準定義的;
  • 其次,使用該屬性會形成嚴重的性能問題。由於 JavaScript 經過隱藏類優化了不少原有的對象結構,因此經過直接修改__proto__會直接破壞現有已經優化的結構,觸發 V8 重構該對象的隱藏類!

構造函數是怎麼建立對象的?

  在 JavaScript 中,使用 new 加上構造函數的這種組合來建立對象和實現對象的繼承。不過使用這種方式隱含的語義過於隱晦。實際上是 JavaScript 爲了吸引 Java 程序員、在語法層面去蹭 Java 熱點,因此就被硬生生地強制加入了很是不協調的關鍵字 new。

function DogFactory(type, color) {
  this.type = type;
  this.color = color;
}
var dog = new DogFactory('Dog', 'Black');

  其實當 V8 執行上面這段代碼時,V8 在背後悄悄地作了如下幾件事情:

var dog = {};
dog.__proto__ = DogFactory.prototype;
DogFactory.call(dog, 'Dog', 'Black');

機器碼、字節碼

V8 爲何要引入字節碼

  • 早期的 V8 爲了提高代碼的執行速度,直接將 JavaScript 源代碼編譯成了沒有優化的二進制機器代碼,若是某一段二進制代碼執行頻率太高,那麼 V8 會將其標記爲熱點代碼,熱點代碼會被優化編譯器優化,優化後的機器代碼執行效率更高。
  • 隨着移動設備的普及,V8 團隊逐漸發現將 JavaScript 源碼直接編譯成二進制代碼存在兩個致命的問題:

    • 時間問題:編譯時間太久,影響代碼啓動速度;
    • 空間問題:緩存編譯後的二進制代碼佔用更多的內存。
  • 這兩個問題無疑會阻礙 V8 在移動設備上的普及,因而 V8 團隊大規模重構代碼,引入了中間的字節碼。字節碼的優點有以下三點:

    • 解決啓動問題:生成字節碼的時間很短;
    • 解決空間問題:字節碼雖然佔用的空間比原始的 JavaScript 多,可是相較於機器代碼,字節碼仍是小了太多,緩存字節碼會大大下降內存的使用。
    • 代碼架構清晰:採用字節碼,能夠簡化程序的複雜度,使得 V8 移植到不一樣的 CPU 架構平臺更加容易。
  • Bytecode 某種程度上就是彙編語言,只是它沒有對應特定的 CPU,或者說它對應的是虛擬的 CPU。這樣的話,生成 Bytecode 時簡單不少,無需爲不一樣的 CPU 生產不一樣的代碼。要知道,V8 支持 9 種不一樣的 CPU,引入一箇中間層 Bytecode,能夠簡化 V8 的編譯流程,提升可擴展性。
  • 若是咱們在不一樣硬件上去生成 Bytecode,會發現生成代碼的指令是同樣的。

如何查看字節碼

// 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 <SharedFunctionInfo add>)]
Parameter count 3 #三個參數,包括了顯式地傳入的 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  #結束當前函數的執行,並將控制權傳回給調用方
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
3

經常使用字節碼指令

  • Ldar:表示將寄存器中的值加載到累加器中,你能夠把它理解爲 LoaD Accumulator from Register,就是把某個寄存器中的值,加載到累加器中。
  • Star:表示 Store Accumulator Register, 你能夠把它理解爲 Store Accumulator to Register,就是把累加器中的值保存到某個寄存器中
  • Add:Add a0, [0]是從 a0 寄存器加載值並將其與累加器中的值相加,而後將結果再次放入累加器。

    add a0 後面的[0]稱之爲 feedback vector slot,又叫 反饋向量槽,它是一個數組,解釋器將解釋執行過程當中的一些數據類型的分析信息都保存在這個反饋向量槽中了,目的是爲了給 TurboFan 優化編譯器提供優化信息,不少字節碼都會爲反饋向量槽提供運行時信息。
  • LdaSmi:將小整數(Smi)加載到累加器寄存器中
  • Return:結束當前函數的執行,並將控制權傳回給調用方。返回的值是累加器中的值。

bytecode-ignition

V8 中的字節碼指令集

隱藏類和內聯緩存

  JavaScript 是一門動態語言,其執行效率要低於靜態語言,V8 爲了提高 JavaScript 的執行速度,借鑑了不少靜態語言的特性,好比實現了 JIT 機制,爲了提高對象的屬性訪問速度而引入了隱藏類,爲了加速運算而引入了內聯緩存

爲何靜態語言的效率更高?

  靜態語言中,如 C++ 在聲明一個對象以前須要定義該對象的結構,代碼在執行以前須要先被編譯,編譯的時候,每一個對象的形狀都是固定的,也就是說,在代碼的執行過程當中是沒法被改變的。能夠直接經過偏移量查詢來查詢對象的屬性值,這也就是靜態語言的執行效率高的一個緣由。

  JavaScript 在運行時,對象的屬性是能夠被修改的,因此當 V8 使用了一個對象時,好比使用了 obj.x 的時候,它並不知道該對象中是否有 x,也不知道 x 相對於對象的偏移量是多少,也就是說 V8 並不知道該對象的具體的形狀。那麼,當在 JavaScript 中要查詢對象 obj 中的 x 屬性時,V8 會按照具體的規則一步一步來查詢,這個過程很是的慢且耗時。

將靜態的特性引入到 V8

  • V8 採用的一個思路就是將 JavaScript 中的對象靜態化,也就是 V8 在運行 JavaScript 的過程當中,會假設 JavaScript 中的對象是靜態的。
  • 具體地講,V8 對每一個對象作以下兩點假設:

    • 對象建立好了以後就不會添加新的屬性;
    • 對象建立好了以後也不會刪除屬性。
  • 符合這兩個假設以後,V8 就能夠對 JavaScript 中的對象作深度優化了。V8 會爲每一個對象建立一個隱藏類,對象的隱藏類中記錄了該對象一些基礎的佈局信息,包括如下兩點:

    • 對象中所包含的全部的屬性;
    • 每一個屬性相對於對象的偏移量。
  • 有了隱藏類以後,那麼當 V8 訪問某個對象中的某個屬性時,就會先去隱藏類中查找該屬性相對於它的對象的偏移量,有了偏移量和屬性類型,V8 就能夠直接去內存中取出對應的屬性值,而不須要經歷一系列的查找過程,那麼這就大大提高了 V8 查找對象的效率。
  • 在 V8 中,把隱藏類又稱爲 map,每一個對象都有一個 map 屬性,其值指向內存中的隱藏類;
  • map 描述了對象的內存佈局,好比對象都包括了哪些屬性,這些數據對應於對象的偏移量是多少。

經過 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 對象建立的隱藏類
 - 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 對象建立的隱藏類
 - 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 對象建立的隱藏類
 - 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

多個對象共用一個隱藏類

  • 在 V8 中,每一個對象都有一個 map 屬性,該屬性值指向該對象的隱藏類。不過若是兩個對象的形狀是相同的,V8 就會爲其複用同一個隱藏類,這樣有兩個好處:

    • 減小隱藏類的建立次數,也間接加速了代碼的執行速度;
    • 減小了隱藏類的存儲空間。
  • 那麼,什麼狀況下兩個對象的形狀是相同的,要知足如下兩點:

    • 相同的屬性名稱;
    • 相等的屬性個數。

從新構建隱藏類

  • 給一個對象添加新的屬性,刪除新的屬性,或者改變某個屬性的數據類型都會改變這個對象的形狀,那麼勢必也就會觸發 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 <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]
 ...
  • 每次給對象添加了一個新屬性以後,該對象的隱藏類的地址都會改變,這也就意味着隱藏類也隨着改變了;若是刪除對象的某個屬性,那麼對象的形狀也就隨着發生了改變,這時 V8 也會重建該對象的隱藏類;
  • 最佳實踐

    • 使用字面量初始化對象時,要保證屬性的順序是一致的;
    • 儘可能使用字面量一次性初始化完整對象屬性;
    • 儘可能避免使用 delete 方法。

經過內聯緩存來提高函數執行效率

  雖然隱藏類可以加速查找對象的速度,可是在 V8 查找對象屬性值的過程當中,依然有查找對象的隱藏類和根據隱藏類來查找對象屬性值的過程。若是一個函數中利用了對象的屬性,而且這個函數會被屢次執行:

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);
  // 產生多態
  loadX(obj2);
}

一般 V8 獲取 obj.x 的流程

  • 找對象 obj 的隱藏類;
  • 再經過隱藏類查找 x 屬性偏移量;
  • 而後根據偏移量獲取屬性值,在這段代碼中 loadX 函數會被反覆執行,那麼獲取 obj.x 的流程也須要反覆被執行;

內聯緩存及其原理

  • 函數 loadX 在一個 for 循環裏面被重複執行了不少次,所以 V8 會想盡一切辦法來壓縮這個查找過程,以提高對象的查找效率。這個加速函數執行的策略就是內聯緩存 (Inline Cache),簡稱爲 IC;
  • IC 的原理:在 V8 執行函數的過程當中,會觀察函數中一些調用點 (CallSite) 上的關鍵中間數據,而後將這些數據緩存起來,當下次再次執行該函數的時候,V8 就能夠直接利用這些中間數據,節省了再次獲取這些數據的過程,所以 V8 利用 IC,能夠有效提高一些重複代碼的執行效率。
  • IC 會爲每一個函數維護一個反饋向量 (FeedBack Vector),反饋向量記錄了函數在執行過程當中的一些關鍵的中間數據。
  • 反饋向量其實就是一個表結構,它由不少項組成的,每一項稱爲一個插槽 (Slot),V8 會依次將執行 loadX 函數的中間數據寫入到反饋向量的插槽中。
  • 當 V8 再次調用 loadX 函數時,好比執行到 loadX 函數中的 return obj.x 語句時,它就會在對應的插槽中查找 x 屬性的偏移量,以後 V8 就能直接去內存中獲取 obj.x 的屬性值了。這樣就大大提高了 V8 的執行效率。

單態、多態和超態

  • 若是一個插槽中只包含 1 個隱藏類,那麼咱們稱這種狀態爲單態 (monomorphic);
  • 若是一個插槽中包含了 2 ~ 4 個隱藏類,那咱們稱這種狀態爲多態 (polymorphic);
  • 若是一個插槽中超過 4 個隱藏類,那咱們稱這種狀態爲超態 (magamorphic)。
  • 單態的性能優於多態和超態,因此咱們須要稍微避免多態和超態的狀況。要避免多態和超態,那麼就儘可能默認全部的對象屬性是不變的,好比你寫了一個 loadX(obj) 的函數,那麼當傳遞參數時,儘可能不要使用多個不一樣形狀的 obj 對象。

總結:
  V8 引入了內聯緩存(IC),IC 會監聽每一個函數的執行過程,並在一些關鍵的地方埋下監聽點,這些包括了加載對象屬性 (Load)、給對象屬性賦值 (Store)、還有函數調用 (Call),V8 會將監聽到的數據寫入一個稱爲反饋向量 (FeedBack Vector) 的結構中,同時 V8 會爲每一個執行的函數維護一個反饋向量。有了反饋向量緩存的臨時數據,V8 就能夠縮短對象屬性的查找路徑,從而提高執行效率。可是針對函數中的同一段代碼,若是對象的隱藏類是不一樣的,那麼反饋向量也會記錄這些不一樣的隱藏類,這就出現了多態和超態的狀況。咱們在實際項目中,要儘可能避免出現多態或者超態的狀況

異步編程與消息隊列

V8 是如何執行回調函數的

  回調函數有兩種類型:同步回調和異步回調,同步回調函數是在執行函數內部被執行的,而異步回調函數是在執行函數外部被執行的。
  通用 UI 線程宏觀架構:
通用UI線程架構
  UI 線程提供一個消息隊列,並將待執行的事件添加到消息隊列中,而後 UI 線程會不斷循環地從消息隊列中取出事件、執行事件。關於異步回調,這裏也有兩種不一樣的類型,其典型表明是 setTimeout 和 XMLHttpRequest:

  • setTimeout 的執行流程實際上是比較簡單的,在 setTimeout 函數內部封裝回調消息,並將回調消息添加進消息隊列,而後主線程從消息隊列中取出回調事件,並執行回調函數。
  • XMLHttpRequest 稍微複雜一點,由於下載過程須要放到單獨的一個線程中去執行,因此執行 XMLHttpRequest.send 的時候,宿主會將實際請求轉發給網絡線程,而後 send 函數退出,主線程繼續執行下面的任務。網絡線程在執行下載的過程當中,會將一些中間信息和回調函數封裝成新的消息,並將其添加進消息隊列中,而後主線程從消息隊列中取出回調事件,並執行回調函數。

宏任務和微任務

  • 調用棧:調用棧是一種數據結構,用來管理在主線程上執行的函數的調用關係。主線程在執行任務的過程當中,若是函數的調用層次過深,可能形成棧溢出的錯誤,咱們可使用 setTimeout 來解決棧溢出的問題。setTimeout 的本質是將同步函數調用改爲異步函數調用,這裏的異步調用是將回調函數封裝成宏任務,並將其添加進消息隊列中,而後主線程再按照必定規則循環地從消息隊列中讀取下一個宏任務。
  • 宏任務:就是指消息隊列中的等待被主線程執行的事件。每一個宏任務在執行時,V8 都會從新建立棧,而後隨着宏任務中函數調用,棧也隨之變化,最終,當該宏任務執行結束時,整個棧又會被清空,接着主線程繼續執行下一個宏任務。
  • 微任務:你能夠把微任務當作是一個須要異步執行的函數,執行時機是在主函數執行結束以後、當前宏任務結束以前。
  • JavaScript 中之因此要引入微任務,主要是因爲主線程執行消息隊列中宏任務的時間顆粒度太粗了,沒法勝任一些對精度和實時性要求較高的場景,微任務能夠在實時性和效率之間作一個有效的權衡。另外使用微任務,能夠改變咱們如今的異步編程模型,使得咱們可使用同步形式的代碼來編寫異步調用。
  • 微任務是基於消息隊列、事件循環、UI 主線程還有堆棧而來的,而後基於微任務,又能夠延伸出協程、Promise、Generator、await/async 等現代前端常用的一些技術。

    微任務技術棧

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

    調用棧、主線程、消息隊列
    微任務:

// 瀏覽器console控制檯可以使瀏覽器卡死(沒法響應鼠標事件等)
function foo() {
  return Promise.resolve().then(foo);
}
foo();
  • 若是當前的任務中產生了一個微任務,經過 Promise.resolve() 或者 Promise.reject() 都會觸發微任務,觸發的微任務不會在當前的函數中被執行,因此*執行微任務時,不會致使棧的無限擴張
  • 和異步調用不一樣,微任務依然會在當前任務執行結束以前被執行,這也就意味着在當前微任務執行結束以前,消息隊列中的其餘任務是不可能被執行的。所以在函數內部觸發的微任務,必定比在函數內部觸發的宏任務要優先執行。
  • 微任務依然是在當前的任務中執行的,因此若是在微任務中循環觸發新的微任務,那麼將致使消息隊列中的其餘任務沒有機會被執行。

前端異步編程方案史

前端異步編程方案史

  • Callback 模式的異步編程模型須要實現大量的回調函數,大量的回調函數會打亂代碼的正常邏輯,使得代碼變得不線性、不易閱讀,這就是咱們所說的回調地獄問題
  • Promise 能很好地解決回調地獄的問題,咱們能夠按照線性的思路來編寫代碼,這個過程是線性的,很是符合人的直覺。
  • 可是這種方式充滿了 Promise 的 then() 方法,若是處理流程比較複雜的話,那麼整段代碼將充斥着大量的 then,語義化不明顯,代碼不能很好地表示執行流程。咱們想要經過線性的方式來編寫異步代碼,要實現這個理想,最關鍵的是要能實現函數暫停和恢復執行的功能。而生成器就能夠實現函數暫停和恢復,咱們能夠在生成器中使用同步代碼的邏輯來異步代碼 (實現該邏輯的核心是協程)。
  • 可是在生成器以外,咱們還須要一個觸發器來驅動生成器的執行。前端的最終方案就是 async/await,async 是一個能夠暫停和恢復執行的函數,在 async 函數內部使用 await 來暫停 async 函數的執行,await 等待的是一個 Promise 對象,若是 Promise 的狀態變成 resolve 或者 reject,那麼 async 函數會恢復執行。所以,使用 async/await 能夠實現以同步的方式編寫異步代碼這一目標。和生成器函數同樣,使用了 async 聲明的函數在執行時,也是一個單獨的協程,咱們可使用 await 來暫停該協程,因爲 await 等待的是一個 Promise 對象,咱們能夠 resolve 來恢復該協程。
協程是一種比線程更加輕量級的存在。你能夠把協程當作是跑在線程上的任務,一個線程上能夠存在多個協程,可是在線程上同時只能執行一個協程。好比,當前執行的是 A 協程,要啓動 B 協程,那麼 A 協程就須要將主線程的控制權交給 B 協程,這就體如今 A 協程暫停執行,B 協程恢復執行;一樣,也能夠從 B 協程中啓動 A 協程。一般,若是從 A 協程啓動 B 協程,咱們就把 A 協程稱爲 B 協程的父協程。

正如一個進程能夠擁有多個線程同樣,一個線程也能夠擁有多個協程。每一時刻,該線程只能執行其中某一個協程。最重要的是,協程不是被操做系統內核所管理,而徹底是由程序所控制(也就是在用戶態執行)。這樣帶來的好處就是性能獲得了很大的提高,不會像線程切換那樣消耗資源。

資料拓展:co 函數庫的含義和用法

垃圾回收

垃圾數據

  從「GC Roots」對象出發,遍歷 GC Root 中的全部對象,若是經過 GC Roots 沒有遍歷到的對象,則這些對象即是垃圾數據。V8 會有專門的垃圾回收器來回收這些垃圾數據。

垃圾回收算法

垃圾回收大體能夠分爲如下幾個步驟:

  • 第一步,經過 GC Root 標記空間中活動對象和非活動對象。目前 V8 採用的可訪問性(reachability)算法來判斷堆中的對象是不是活動對象。具體地講,這個算法是將一些 GC Root 做爲初始存活的對象的集合,從 GC Roots 對象出發,遍歷 GC Root 中的全部對象:

    • 經過 GC Root 遍歷到的對象,咱們就認爲該對象是可訪問的(reachable),那麼必須保證這些對象應該在內存中保留,咱們也稱可訪問的對象爲活動對象
    • 經過 GC Roots 沒有遍歷到的對象,則是不可訪問的(unreachable),那麼這些不可訪問的對象就可能被回收,咱們稱不可訪問的對象爲非活動對象
    • 瀏覽器環境中,GC Root 有不少,一般包括瞭如下幾種 (可是不止於這幾種):

      • 全局的 window 對象(位於每一個 iframe 中);
      • 文檔 DOM 樹,由能夠經過遍歷文檔到達的全部原生 DOM 節點組成;
      • 存放棧上變量。
  • 第二步,回收非活動對象所佔據的內存。其實就是在全部的標記完成以後,統一清理內存中全部被標記爲可回收的對象。
  • 第三步,作內存整理。通常來講,頻繁回收對象後,內存中就會存在大量不連續空間,咱們把這些不連續的內存空間稱爲內存碎片。當內存中出現了大量的內存碎片以後,若是須要分配較大的連續內存時,就有可能出現內存不足的狀況,因此最後一步須要整理這些內存碎片。但這步實際上是可選的,由於有的垃圾回收器不會產生內存碎片(好比副垃圾回收器)

垃圾回收

  • V8 依據代際假說,將堆內存劃分爲新生代和老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放生存時間久的對象。代際假說有兩個特色:

    • 第一個是大部分對象都是「朝生夕死」的,也就是說大部分對象在內存中存活的時間很短,好比函數內部聲明的變量,或者塊級做用域中的變量,當函數或者代碼塊執行結束時,做用域中定義的變量就會被銷燬。所以這一類對象一經分配內存,很快就變得不可訪問;
    • 第二個是不死的對象,會活得更久,好比全局的 window、DOM、Web API 等對象。
  • 爲了提高垃圾回收的效率,V8 設置了兩個垃圾回收器,主垃圾回收器和副垃圾回收器。

    • 主垃圾回收器負責收集老生代中的垃圾數據,副垃圾回收器負責收集新生代中的垃圾數據。
    • 副垃圾回收器採用了 Scavenge 算法,是把新生代空間對半劃分爲兩個區域(有些地方也稱做From和To空間),一半是對象區域,一半是空閒區域。新的數據都分配在對象區域,等待對象區域快分配滿的時候,垃圾回收器便執行垃圾回收操做,以後將存活的對象從對象區域拷貝到空閒區域,並將兩個區域互換。

      • 這種角色翻轉的操做還能讓新生代中的這兩塊區域無限重複使用下去。
      • 副垃圾回收器每次執行清理操做時,都須要將存活的對象從對象區域複製到空閒區域,複製操做須要時間成本,若是新生區空間設置得太大了,那麼每次清理的時間就會太久,因此爲了執行效率,通常新生區的空間會被設置得比較小
      • 副垃圾回收器還會採用對象晉升策略,也就是移動那些通過兩次垃圾回收依然還存活的對象到老生代中。
    • 主垃圾回收器回收器主要負責老生代中的垃圾數據的回收操做,會經歷標記、清除和整理過程

      • 主垃圾回收器主要負責老生代中的垃圾回收。除了新生代中晉升的對象,一些大的對象會直接被分配到老生代裏。
      • 老生代中的對象有兩個特色:一個是對象佔用空間大;另外一個是對象存活時間長。

Stop-The-World

  因爲 JavaScript 是運行在主線程之上的,所以,一旦執行垃圾回收算法,都須要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢後再恢復腳本執行。咱們把這種行爲叫作全停頓(Stop-The-World)

  • V8 最開始的垃圾回收器有兩個特色:

    • 第一個是垃圾回收在主線程上執行,
    • 第二個特色是一次執行一個完整的垃圾回收流程。
  • 因爲這兩個緣由,很容易形成主線程卡頓,因此 V8 採用了不少優化執行效率的方案。

    • 第一個方案是並行回收,在執行一個完整的垃圾回收過程當中,垃圾回收器會使用多個輔助線程來並行執行垃圾回收。
    • 第二個方案是增量式垃圾回收,垃圾回收器將標記工做分解爲更小的塊,而且穿插在主線程不一樣的任務之間執行。採用增量垃圾回收時,垃圾回收器沒有必要一次執行完整的垃圾回收過程,每次執行的只是整個垃圾回收過程當中的一小部分工做。
    • 第三個方案是併發回收,回收線程在執行 JavaScript 的過程,輔助線程可以在後臺完成的執行垃圾回收的操做。
    • 主垃圾回收器就綜合採用了全部的方案(併發標記,增量標記,輔助清理),副垃圾回收器也採用了部分方案。

似此星辰非昨夜,爲誰風露立中宵

Breaking the JavaScript Speed Limit with V8

  Daniel Clifford 在 Google I/O 2012 上作了一個精彩的演講「Breaking the JavaScript Speed Limit with V8」。在演講中,他深刻解釋了 13 個簡單的代碼優化方法,可讓你的JavaScript代碼在 Chrome V8 引擎編譯/運行時更加快速。在演講中,他介紹了怎麼優化,並解釋了緣由。下面簡明的列出了13 個 JavaScript 性能提高技巧

  1. 在構造函數裏初始化全部對象的成員(因此這些實例以後不會改變其隱藏類);
  2. 老是以相同的次序初始化對象成員;
  3. 儘可能使用能夠用 31 位有符號整數表示的數;
  4. 爲數組使用從 0 開始的連續的主鍵;
  5. 別預分配大數組(好比大於 64K 個元素)到其最大尺寸,令其尺寸順其天然發展就好;
  6. 別刪除數組裏的元素,尤爲是數字數組;
  7. 別加載未初始化或已刪除的元素;
  8. 對於固定大小的數組,使用」array literals「初始化(初始化小額定長數組時,用字面量進行初始化);
  9. 小數組(小於 64k)在使用以前先預分配正確的尺寸;
  10. 請勿在數字數組中存放非數字的值(對象);
  11. 儘可能使用單一類型(monomorphic)而不是多類型(polymorphic)(若是經過非字面量進行初始化小數組時,切勿觸發類型的從新轉換);
  12. 不要使用 try{} catch{}(若是存在 try/catch 代碼快,則將性能敏感的代碼放到一個嵌套的函數中);
  13. 在優化後避免在方法中修改隱藏類。
演講資料參考: Performance Tips for JavaScript in V8 | 譯文 | 內網視頻 | YouTube

在 V8 引擎裏 5 個優化代碼的技巧

  1. 對象屬性的順序: 在實例化你的對象屬性的時候必定要使用相同的順序,這樣隱藏類和隨後的優化代碼才能共享;
  2. 動態屬性: 在對象實例化以後再添加屬性會強制使得隱藏類變化,而且會減慢爲舊隱藏類所優化的代碼的執行。因此,要在對象的構造函數中完成全部屬性的分配;
  3. 方法: 重複執行相同的方法會運行的比不一樣的方法只執行一次要快 (由於內聯緩存);
  4. 數組: 避免使用 keys 不是遞增的數字的稀疏數組,這種 key 值不是遞增數字的稀疏數組實際上是一個 hash 表。在這種數組中每個元素的獲取都是昂貴的代價。同時,要避免提早申請大數組。最好的作法是隨着你的須要慢慢的增大數組。最後,不要刪除數組中的元素,由於這會使得 keys 變得稀疏;
  5. 標記值 (Tagged values): V8 用 32 位來表示對象和數字。它使用一位來區分它是對象 (flag = 1) 仍是一個整型 (flag = 0),也被叫作小整型(SMI),由於它只有 31 位。而後,若是一個數值大於 31 位,V8 將會對其進行 box 操做,而後將其轉換成 double 型,而且建立一個新的對象來裝這個數。因此,爲了不代價很高的 box 操做,儘可能使用 31 位的有符號數。
資料參考: How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 譯文

JavaScript 啓動性能瓶頸分析與解決方案

資料參考: JavaScript Start-up Performance | JavaScript 啓動性能瓶頸分析與解決方案

抽絲剝繭有窮時,V8 綿綿無絕期

番外篇

  • Chrome插件Console Importer推薦:Easily import JS and CSS resources from Chrome console. (能夠在瀏覽器控制檯安裝 loadsh、moment、jQuery 等庫,在控制檯直接驗證、使用這些庫。)
    效果圖:
    Console Importer

本文首發於我的博客,歡迎指正和star

相關文章
相關標籤/搜索