'深入JavaScript(一)工作原理'

"

平時我們只管寫代碼 然後通過瀏覽器或者Node執行 並未深層次探究 大多數人都聽過引擎 例如V8 也知道JavaScript是一門單線程語言 這篇文章簡述代碼執行過程 後續對每一步進行深入探究

運轉過程

JavaScript代碼的整個執行過程需要 引擎、作用域、編譯器 的合作完成

  • 編譯器
  • 負責詞法分析 語法分析 代碼生成供引擎執行
  • 作用域
  • 收集標識符 維護查詢的一套規則
  • 引擎
  • 程序的編譯執行
"

平時我們只管寫代碼 然後通過瀏覽器或者Node執行 並未深層次探究 大多數人都聽過引擎 例如V8 也知道JavaScript是一門單線程語言 這篇文章簡述代碼執行過程 後續對每一步進行深入探究

運轉過程

JavaScript代碼的整個執行過程需要 引擎、作用域、編譯器 的合作完成

  • 編譯器
  • 負責詞法分析 語法分析 代碼生成供引擎執行
  • 作用域
  • 收集標識符 維護查詢的一套規則
  • 引擎
  • 程序的編譯執行
深入JavaScript(一)工作原理

下面我們通過一個簡單的例子瞭解整個過程

demo

var a=2

  1. 編譯器進行詞法分析語法分析 分解成詞法單元 將詞法單元解析成抽象語法樹
  2. 編譯器將變量 a 存入當前作用域 首先詢問作用域當前是否有變量 a 如果有就忽略該聲明 否則聲明變量 命名為 a 為引擎生成運行時的代碼
  3. 引擎執行過程取詢問作用域當前是否有變量 a 如果有 取出來賦值為2 如果沒有 則作用域取外層作用域尋找 最終找到則賦值2 倘若全局作用域沒有找到該變量 非嚴格模式下作用域幫我們創建一個變量 a 並賦值為2 嚴格模式下則報錯

可以看一下我畫的簡略圖

"

平時我們只管寫代碼 然後通過瀏覽器或者Node執行 並未深層次探究 大多數人都聽過引擎 例如V8 也知道JavaScript是一門單線程語言 這篇文章簡述代碼執行過程 後續對每一步進行深入探究

運轉過程

JavaScript代碼的整個執行過程需要 引擎、作用域、編譯器 的合作完成

  • 編譯器
  • 負責詞法分析 語法分析 代碼生成供引擎執行
  • 作用域
  • 收集標識符 維護查詢的一套規則
  • 引擎
  • 程序的編譯執行
深入JavaScript(一)工作原理

下面我們通過一個簡單的例子瞭解整個過程

demo

var a=2

  1. 編譯器進行詞法分析語法分析 分解成詞法單元 將詞法單元解析成抽象語法樹
  2. 編譯器將變量 a 存入當前作用域 首先詢問作用域當前是否有變量 a 如果有就忽略該聲明 否則聲明變量 命名為 a 為引擎生成運行時的代碼
  3. 引擎執行過程取詢問作用域當前是否有變量 a 如果有 取出來賦值為2 如果沒有 則作用域取外層作用域尋找 最終找到則賦值2 倘若全局作用域沒有找到該變量 非嚴格模式下作用域幫我們創建一個變量 a 並賦值為2 嚴格模式下則報錯

可以看一下我畫的簡略圖

深入JavaScript(一)工作原理

引擎

JavaScript引擎是執行JavaScript代碼的解釋器 其可以實現為標準解釋器或者以某種形式將JavaScript編譯為字節碼的即時編譯器

JavaScript引擎較為流行的例子是Google的V8引擎 在chrome和Node.js中使用

V8引擎主要有兩個主要部分組成:

  • memory Heap(內存堆) ---內存分配地址的地方
  • Call Stack(調用堆棧)---代碼執行的地方

需要注意一點 我們平時使用的一些瀏覽器的API(比如 setTimeout )並非引擎提供 這些由瀏覽器提供API稱為Web API

V8引擎

V8引擎由谷歌構建 且開源 使用C++編寫 其初始目的為了提高web瀏覽器中JavaScript執行的性能 為了獲得速度 V8將JavaScript代碼轉換為機器碼 並非使用解釋器 ( JavaScript是一門編譯語言 )一步到位 不生成字節碼或者任何中間代碼

前期階段

V8在5.9版本之前使用兩個編譯器

full-codegen
Crankshaft

V8內部多線程

  • 主線程執行我們的操作:獲取代碼 執行代碼
  • 專門的線程用於編譯 主線程可以在其優化代碼的同時執行代碼
  • Profiler 線程告訴我們運行花費時間 讓 Crankshaft 優化
  • 一些線程進行垃圾回收(處理垃圾收集器)
  1. 當我們第一次執行JavaScript代碼時 V8利用 full-codegen 編譯器 將解析的JavaScript編譯成機器碼不再轉換 快遞執行機器代碼( V8沒有中間字節碼 不需要解釋器
  2. 一段時間後分析線程收集足夠的數據判斷優化方法
  3. Crankshaft 從其他線程開始優化 將JavaScript抽象語法樹轉換為稱作 Hydrogen 的高級靜態單分配表示 並進行優化 Hydrogen 圖

隱藏類

JavaScript並沒有真正意義的繼承(基於原型)即沒有克隆過程

大多數JavaScript解釋器使用類似字典的結構存儲對象屬性值在內存中的位置 但這種結構在JavaScript中檢查屬性的值計算成本較高(JavaScript是一門動態語言 我們可以在實例化後在對象中添加屬性)

靜態語言(例如Go)屬性值可以作為連續緩衝區存儲在存儲器中 (便於查找 您可以仔細想想)每個緩衝區之間有固定偏移量 可以根據屬性類型確定偏移的長度 但是JavaScript運行時可以隨便更改屬性類型 這種方式不行

故V8使用了 隱藏類 隱藏類與其他語言中工作方式類似 不過它們在運行時創建 下面看個例子

function X(a, b) {
this.a = a
this.b = b
}
let x = new X (1, 2)
複製代碼
  • 當 new X(1,2) 調用 開始將創建一個 C0 的隱藏類 該類不帶有任何屬性 (尚未定義屬性 )
  • this.a = a 語句執行 引擎生成 C1 的過渡隱藏類 它基於 C0 其描述了找到屬性 a 的存儲器的位置(有點指針的意思)即記錄屬性的偏移量 它的存在是為了在多個對象間能夠共享隱藏類
  • 這是 a 存儲在偏移0位置 即當存儲器中x對象視為連續緩衝區時 第一偏移對應屬性 a
  • 當我們添加屬性時 引擎使用 類轉換 更新 C1 即 this.b = b 語句執行 隱藏類應從 a 對應的 C1 切換到 C2 ( 允許以相同方式創建的對象之間共享隱藏類 若兩個對象共享一個隱藏類且同一屬性添加到它們 確保兩者都接受到相同的新隱藏類以及攜帶的所有代碼 )

不同初始化順序的對象 隱藏類的使用情況也是不同的 故開發中儘量保持屬性初始化的順序 這樣隱藏類即可共享 思想下面的代碼執行有什麼不同效果

function Person(name, age) {
this.name = name;
this.age = age;
}
var laowang = new Person("隔壁老王", 32);
var laozhang = new Person("專家老張", 20);
laowang.email = "[email protected]";
laowang.job = "無業遊民";
laozhang.job = "程序員";
laozhang.email = "[email protected]";
----------------------------------------
function Person(name, age) {
this.name = name;
this.age = age;
}
var laowang = new Person("隔壁老王", 32);
var laozhang = new Person("專家老張", 20);
laowang.job = "無業遊民";
laozhang.job = "程序員";
laozhang.email = "[email protected]";
laowang.email = "[email protected]";
複製代碼

內聯代碼及內聯緩存

前面我們說過優化 第一個優化是提前內聯儘可能多的代碼 內聯是用被調用函數的主體替換調用點的過程 看圖就懂

"

平時我們只管寫代碼 然後通過瀏覽器或者Node執行 並未深層次探究 大多數人都聽過引擎 例如V8 也知道JavaScript是一門單線程語言 這篇文章簡述代碼執行過程 後續對每一步進行深入探究

運轉過程

JavaScript代碼的整個執行過程需要 引擎、作用域、編譯器 的合作完成

  • 編譯器
  • 負責詞法分析 語法分析 代碼生成供引擎執行
  • 作用域
  • 收集標識符 維護查詢的一套規則
  • 引擎
  • 程序的編譯執行
深入JavaScript(一)工作原理

下面我們通過一個簡單的例子瞭解整個過程

demo

var a=2

  1. 編譯器進行詞法分析語法分析 分解成詞法單元 將詞法單元解析成抽象語法樹
  2. 編譯器將變量 a 存入當前作用域 首先詢問作用域當前是否有變量 a 如果有就忽略該聲明 否則聲明變量 命名為 a 為引擎生成運行時的代碼
  3. 引擎執行過程取詢問作用域當前是否有變量 a 如果有 取出來賦值為2 如果沒有 則作用域取外層作用域尋找 最終找到則賦值2 倘若全局作用域沒有找到該變量 非嚴格模式下作用域幫我們創建一個變量 a 並賦值為2 嚴格模式下則報錯

可以看一下我畫的簡略圖

深入JavaScript(一)工作原理

引擎

JavaScript引擎是執行JavaScript代碼的解釋器 其可以實現為標準解釋器或者以某種形式將JavaScript編譯為字節碼的即時編譯器

JavaScript引擎較為流行的例子是Google的V8引擎 在chrome和Node.js中使用

V8引擎主要有兩個主要部分組成:

  • memory Heap(內存堆) ---內存分配地址的地方
  • Call Stack(調用堆棧)---代碼執行的地方

需要注意一點 我們平時使用的一些瀏覽器的API(比如 setTimeout )並非引擎提供 這些由瀏覽器提供API稱為Web API

V8引擎

V8引擎由谷歌構建 且開源 使用C++編寫 其初始目的為了提高web瀏覽器中JavaScript執行的性能 為了獲得速度 V8將JavaScript代碼轉換為機器碼 並非使用解釋器 ( JavaScript是一門編譯語言 )一步到位 不生成字節碼或者任何中間代碼

前期階段

V8在5.9版本之前使用兩個編譯器

full-codegen
Crankshaft

V8內部多線程

  • 主線程執行我們的操作:獲取代碼 執行代碼
  • 專門的線程用於編譯 主線程可以在其優化代碼的同時執行代碼
  • Profiler 線程告訴我們運行花費時間 讓 Crankshaft 優化
  • 一些線程進行垃圾回收(處理垃圾收集器)
  1. 當我們第一次執行JavaScript代碼時 V8利用 full-codegen 編譯器 將解析的JavaScript編譯成機器碼不再轉換 快遞執行機器代碼( V8沒有中間字節碼 不需要解釋器
  2. 一段時間後分析線程收集足夠的數據判斷優化方法
  3. Crankshaft 從其他線程開始優化 將JavaScript抽象語法樹轉換為稱作 Hydrogen 的高級靜態單分配表示 並進行優化 Hydrogen 圖

隱藏類

JavaScript並沒有真正意義的繼承(基於原型)即沒有克隆過程

大多數JavaScript解釋器使用類似字典的結構存儲對象屬性值在內存中的位置 但這種結構在JavaScript中檢查屬性的值計算成本較高(JavaScript是一門動態語言 我們可以在實例化後在對象中添加屬性)

靜態語言(例如Go)屬性值可以作為連續緩衝區存儲在存儲器中 (便於查找 您可以仔細想想)每個緩衝區之間有固定偏移量 可以根據屬性類型確定偏移的長度 但是JavaScript運行時可以隨便更改屬性類型 這種方式不行

故V8使用了 隱藏類 隱藏類與其他語言中工作方式類似 不過它們在運行時創建 下面看個例子

function X(a, b) {
this.a = a
this.b = b
}
let x = new X (1, 2)
複製代碼
  • 當 new X(1,2) 調用 開始將創建一個 C0 的隱藏類 該類不帶有任何屬性 (尚未定義屬性 )
  • this.a = a 語句執行 引擎生成 C1 的過渡隱藏類 它基於 C0 其描述了找到屬性 a 的存儲器的位置(有點指針的意思)即記錄屬性的偏移量 它的存在是為了在多個對象間能夠共享隱藏類
  • 這是 a 存儲在偏移0位置 即當存儲器中x對象視為連續緩衝區時 第一偏移對應屬性 a
  • 當我們添加屬性時 引擎使用 類轉換 更新 C1 即 this.b = b 語句執行 隱藏類應從 a 對應的 C1 切換到 C2 ( 允許以相同方式創建的對象之間共享隱藏類 若兩個對象共享一個隱藏類且同一屬性添加到它們 確保兩者都接受到相同的新隱藏類以及攜帶的所有代碼 )

不同初始化順序的對象 隱藏類的使用情況也是不同的 故開發中儘量保持屬性初始化的順序 這樣隱藏類即可共享 思想下面的代碼執行有什麼不同效果

function Person(name, age) {
this.name = name;
this.age = age;
}
var laowang = new Person("隔壁老王", 32);
var laozhang = new Person("專家老張", 20);
laowang.email = "[email protected]";
laowang.job = "無業遊民";
laozhang.job = "程序員";
laozhang.email = "[email protected]";
----------------------------------------
function Person(name, age) {
this.name = name;
this.age = age;
}
var laowang = new Person("隔壁老王", 32);
var laozhang = new Person("專家老張", 20);
laowang.job = "無業遊民";
laozhang.job = "程序員";
laozhang.email = "[email protected]";
laowang.email = "[email protected]";
複製代碼

內聯代碼及內聯緩存

前面我們說過優化 第一個優化是提前內聯儘可能多的代碼 內聯是用被調用函數的主體替換調用點的過程 看圖就懂

深入JavaScript(一)工作原理

僅僅依靠隱藏類還不夠 引擎執行過程還需查找隱藏類 故V8加入 內聯緩存 技術優化運行時查找對象及其屬性的過程 核心原理即在運行過程中 收集類型信息 從而可以在後續運行中利用這些信息進行預判(有點機器學習的感覺 哈哈 有個大膽的想法 )

V8維護在最近方法調用中作為參數傳遞的對象類型的緩存 並使用這些信息預測將來作為參數傳遞的對象類型 這樣即可使用從以前的查找到對象的隱藏類的存儲信息

無論何時在特定對象調用方法 V8引擎必須執行對該對象的隱藏類的查找 確定訪問特定屬性的偏移量 當同一個隱藏類的兩次方法都調用之後 V8就會省略對隱藏類的查找 直接簡單的將該屬性的偏移量添加到對象指針本身 那麼對於該方法的下一次調用 V8將假定隱藏類沒有改變 使用從以前的查找存儲的偏移量直接跳轉到特定屬性的內存地址

這也解釋了為什麼相同類型的對象共享隱藏類 如果你創建兩個相同類型和不同隱藏類的對象 V8將無法使用內聯緩存 因為即使這兩個對象屬於同一類型 它們對應的隱藏類為其屬性分配不同的偏移量

肯定有人想問 如果類型在程序執行中發生變化怎麼辦????

對於這種情況 內聯緩存會在直接調用之前驗證類型 這些驗證類型的代碼叫做 **前導代碼**

var arr= [2,4,6,8];
arr,foreach((item) => console.log(item.toString()))
複製代碼

上面的代碼 arrr[0] 在第一次 toString 方法時發起一次動態查詢 記錄查詢結果 當後續調用 toString 方法時 引擎根據上次的記錄直接獲知調用點 不再進行動態查詢操作 但是問題來了

var arr=[2,'4',6,'8']
arr.foreach(item => console.log(item.toString()))
複製代碼

可以看到 對象類型經常發生變化 這就會導致緩存失效 為了防止這種情況 V8採用了 polymorphic inline cache(PIC) (多態內聯緩存)技術 它不僅只存儲上一次查詢結果 而是會緩存所有多態調用點中的查詢結果 並把這個結果存儲在一個特別產生的樁函數 這個樁函數實際上會在調用點位置上代替原有的調用點

"

平時我們只管寫代碼 然後通過瀏覽器或者Node執行 並未深層次探究 大多數人都聽過引擎 例如V8 也知道JavaScript是一門單線程語言 這篇文章簡述代碼執行過程 後續對每一步進行深入探究

運轉過程

JavaScript代碼的整個執行過程需要 引擎、作用域、編譯器 的合作完成

  • 編譯器
  • 負責詞法分析 語法分析 代碼生成供引擎執行
  • 作用域
  • 收集標識符 維護查詢的一套規則
  • 引擎
  • 程序的編譯執行
深入JavaScript(一)工作原理

下面我們通過一個簡單的例子瞭解整個過程

demo

var a=2

  1. 編譯器進行詞法分析語法分析 分解成詞法單元 將詞法單元解析成抽象語法樹
  2. 編譯器將變量 a 存入當前作用域 首先詢問作用域當前是否有變量 a 如果有就忽略該聲明 否則聲明變量 命名為 a 為引擎生成運行時的代碼
  3. 引擎執行過程取詢問作用域當前是否有變量 a 如果有 取出來賦值為2 如果沒有 則作用域取外層作用域尋找 最終找到則賦值2 倘若全局作用域沒有找到該變量 非嚴格模式下作用域幫我們創建一個變量 a 並賦值為2 嚴格模式下則報錯

可以看一下我畫的簡略圖

深入JavaScript(一)工作原理

引擎

JavaScript引擎是執行JavaScript代碼的解釋器 其可以實現為標準解釋器或者以某種形式將JavaScript編譯為字節碼的即時編譯器

JavaScript引擎較為流行的例子是Google的V8引擎 在chrome和Node.js中使用

V8引擎主要有兩個主要部分組成:

  • memory Heap(內存堆) ---內存分配地址的地方
  • Call Stack(調用堆棧)---代碼執行的地方

需要注意一點 我們平時使用的一些瀏覽器的API(比如 setTimeout )並非引擎提供 這些由瀏覽器提供API稱為Web API

V8引擎

V8引擎由谷歌構建 且開源 使用C++編寫 其初始目的為了提高web瀏覽器中JavaScript執行的性能 為了獲得速度 V8將JavaScript代碼轉換為機器碼 並非使用解釋器 ( JavaScript是一門編譯語言 )一步到位 不生成字節碼或者任何中間代碼

前期階段

V8在5.9版本之前使用兩個編譯器

full-codegen
Crankshaft

V8內部多線程

  • 主線程執行我們的操作:獲取代碼 執行代碼
  • 專門的線程用於編譯 主線程可以在其優化代碼的同時執行代碼
  • Profiler 線程告訴我們運行花費時間 讓 Crankshaft 優化
  • 一些線程進行垃圾回收(處理垃圾收集器)
  1. 當我們第一次執行JavaScript代碼時 V8利用 full-codegen 編譯器 將解析的JavaScript編譯成機器碼不再轉換 快遞執行機器代碼( V8沒有中間字節碼 不需要解釋器
  2. 一段時間後分析線程收集足夠的數據判斷優化方法
  3. Crankshaft 從其他線程開始優化 將JavaScript抽象語法樹轉換為稱作 Hydrogen 的高級靜態單分配表示 並進行優化 Hydrogen 圖

隱藏類

JavaScript並沒有真正意義的繼承(基於原型)即沒有克隆過程

大多數JavaScript解釋器使用類似字典的結構存儲對象屬性值在內存中的位置 但這種結構在JavaScript中檢查屬性的值計算成本較高(JavaScript是一門動態語言 我們可以在實例化後在對象中添加屬性)

靜態語言(例如Go)屬性值可以作為連續緩衝區存儲在存儲器中 (便於查找 您可以仔細想想)每個緩衝區之間有固定偏移量 可以根據屬性類型確定偏移的長度 但是JavaScript運行時可以隨便更改屬性類型 這種方式不行

故V8使用了 隱藏類 隱藏類與其他語言中工作方式類似 不過它們在運行時創建 下面看個例子

function X(a, b) {
this.a = a
this.b = b
}
let x = new X (1, 2)
複製代碼
  • 當 new X(1,2) 調用 開始將創建一個 C0 的隱藏類 該類不帶有任何屬性 (尚未定義屬性 )
  • this.a = a 語句執行 引擎生成 C1 的過渡隱藏類 它基於 C0 其描述了找到屬性 a 的存儲器的位置(有點指針的意思)即記錄屬性的偏移量 它的存在是為了在多個對象間能夠共享隱藏類
  • 這是 a 存儲在偏移0位置 即當存儲器中x對象視為連續緩衝區時 第一偏移對應屬性 a
  • 當我們添加屬性時 引擎使用 類轉換 更新 C1 即 this.b = b 語句執行 隱藏類應從 a 對應的 C1 切換到 C2 ( 允許以相同方式創建的對象之間共享隱藏類 若兩個對象共享一個隱藏類且同一屬性添加到它們 確保兩者都接受到相同的新隱藏類以及攜帶的所有代碼 )

不同初始化順序的對象 隱藏類的使用情況也是不同的 故開發中儘量保持屬性初始化的順序 這樣隱藏類即可共享 思想下面的代碼執行有什麼不同效果

function Person(name, age) {
this.name = name;
this.age = age;
}
var laowang = new Person("隔壁老王", 32);
var laozhang = new Person("專家老張", 20);
laowang.email = "[email protected]";
laowang.job = "無業遊民";
laozhang.job = "程序員";
laozhang.email = "[email protected]";
----------------------------------------
function Person(name, age) {
this.name = name;
this.age = age;
}
var laowang = new Person("隔壁老王", 32);
var laozhang = new Person("專家老張", 20);
laowang.job = "無業遊民";
laozhang.job = "程序員";
laozhang.email = "[email protected]";
laowang.email = "[email protected]";
複製代碼

內聯代碼及內聯緩存

前面我們說過優化 第一個優化是提前內聯儘可能多的代碼 內聯是用被調用函數的主體替換調用點的過程 看圖就懂

深入JavaScript(一)工作原理

僅僅依靠隱藏類還不夠 引擎執行過程還需查找隱藏類 故V8加入 內聯緩存 技術優化運行時查找對象及其屬性的過程 核心原理即在運行過程中 收集類型信息 從而可以在後續運行中利用這些信息進行預判(有點機器學習的感覺 哈哈 有個大膽的想法 )

V8維護在最近方法調用中作為參數傳遞的對象類型的緩存 並使用這些信息預測將來作為參數傳遞的對象類型 這樣即可使用從以前的查找到對象的隱藏類的存儲信息

無論何時在特定對象調用方法 V8引擎必須執行對該對象的隱藏類的查找 確定訪問特定屬性的偏移量 當同一個隱藏類的兩次方法都調用之後 V8就會省略對隱藏類的查找 直接簡單的將該屬性的偏移量添加到對象指針本身 那麼對於該方法的下一次調用 V8將假定隱藏類沒有改變 使用從以前的查找存儲的偏移量直接跳轉到特定屬性的內存地址

這也解釋了為什麼相同類型的對象共享隱藏類 如果你創建兩個相同類型和不同隱藏類的對象 V8將無法使用內聯緩存 因為即使這兩個對象屬於同一類型 它們對應的隱藏類為其屬性分配不同的偏移量

肯定有人想問 如果類型在程序執行中發生變化怎麼辦????

對於這種情況 內聯緩存會在直接調用之前驗證類型 這些驗證類型的代碼叫做 **前導代碼**

var arr= [2,4,6,8];
arr,foreach((item) => console.log(item.toString()))
複製代碼

上面的代碼 arrr[0] 在第一次 toString 方法時發起一次動態查詢 記錄查詢結果 當後續調用 toString 方法時 引擎根據上次的記錄直接獲知調用點 不再進行動態查詢操作 但是問題來了

var arr=[2,'4',6,'8']
arr.foreach(item => console.log(item.toString()))
複製代碼

可以看到 對象類型經常發生變化 這就會導致緩存失效 為了防止這種情況 V8採用了 polymorphic inline cache(PIC) (多態內聯緩存)技術 它不僅只存儲上一次查詢結果 而是會緩存所有多態調用點中的查詢結果 並把這個結果存儲在一個特別產生的樁函數 這個樁函數實際上會在調用點位置上代替原有的調用點

深入JavaScript(一)工作原理

調用棧

JavaScript是一門單線程語言 這就意味著只能有一個調用棧 它一次只能做一件事

調用棧記錄我們程序執行的位置 如果我們運行到一個函數 它就會被推入棧 當從這個函數返回時 它會從棧頂彈出

舉個栗子

function a (x,y) {
return x + y
}
function b(x) {
var c=a(x,x)
console.log(c)
}
b(1)
複製代碼
  • 函數 b 推入棧中
  • 函數 a 推入棧中 此時調用棧有函數 a 和 b 兩個
  • 函數 a 執行完畢了 推出執行棧 console.log(c) 推入棧中
  • console.log(c) 執行完畢推出棧
  • 函數 b 執行完畢了 推出棧 此時棧為空

每一次進入調用棧的稱為調用幀

某些時候我們調用棧堆中的函數調用數量超過調用棧堆的實際大小 瀏覽器會採取行動 拋出異常

深入編譯

一旦 Hydrogen 圖被優化 Crankshaft 將其降低到稱為 Lithium 的低級表示 其實現都是特定架構 寄存器分配大多發生在這個級別

最後 Lithium 被編譯成機器碼 然後就是 OSR:on-stack-replacement (堆棧替換) 我們開始編譯和優化一個方法之前 我們可能會運行堆棧替換 V8不只是緩慢執行堆棧替換 並再次開始優化 而是轉換我們擁有的所有上下文 (堆棧、寄存器) 以便在執行過程中切換到優化版本 具體就不太深入了 (其實是我看不懂)

垃圾收回

標記清除

當變量進入環境 (例如在函數中聲明一個變量)時 將這個變量標記為 進入環境 從邏輯上講 永遠不能釋放進入環境的變量所佔用的內存 只要進入相應的環境 就可能會用到它們 當變量離開環境時 標記為 離開環境

可以使用任何方式來標記變量 比如 可以通過翻轉某個特殊的位來記錄一個變量何時進入環境 或者使用一個“進入環境的”變量列表及一個“離開環境的”變量列表來跟蹤哪個變量發生了變化 如何標記變量並不重要 關鍵在於採取什麼策略

  • 垃圾收集器在運行的時候會給存儲在內存中的所有變量加上標記
  • 然後 它會去掉運行環境中的變量以及被環境變量引用的變量的標記
  • 此後 如果依然有標記的變量就視為準備刪除的變量 原因是運行環境中已經無法訪問這些變量
  • 最後 垃圾收集器完成內存清除工作 銷燬那些帶標記的值並回收它們所佔用的內存空間

引用計數

引用計數的垃圾收集策略不太常見。含義是跟蹤記錄每個值被引用的次數。當聲明瞭一個變量並將一個引用類型值賦給該變量時,則這個值的引用次數就是1。

如果同一個值又被賦給另一個變量,則該值的引用次數加1。相反,如果包含對這個值引用的變量改變了引用對象,則該值引用次數減1。 當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的內存空間回收回來。 這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數為0的值所佔用的內存。

V8垃圾回收策略

V8採用了一種代回收的策略 將內存分為兩個生代: 新生代老生代 新生代的對象為存活時間較短的對象,老生代中的對象為存活時間較長或常駐內存的對象。分別對新生代和老生代使用不同的垃圾回收算法來提升垃圾回收的效率。對象起初都會被分配到新生代,當新生代中的對象滿足某些條件(後面會有介紹)時,會被移動到老生代(晉升)

新生代

大多數都得對象被分配這裡 這個區域很小 但是垃圾回收頻繁 分配內存很容易 我們只需要保存一個指向內存區的指針 不斷根據新對象的大小進行遞增即可 當該指針到達了新生代內存區的末尾 就會有有一次清除(僅為新生代)

回收算法

新生代使用Scavenge算法進行回收。在Scavenge算法的實現中,主要採用了Cheney算法

Cheney算法算法是一種採用複製的方式實現的垃圾回收算法。它將內存一分為二,每一部分空間稱為semispace。在這兩個semispace中,一個處於使用狀態,另一個處於閒置狀態。處於使用狀態的semispace空間稱為From空間,處於閒置狀態的空間稱為To空間,當我們分配對象時,先是在From空間中進行分配。當開始進行垃圾回收算法時,會檢查From空間中的存活對象,這些存活對象將會被複制到To空間中(複製完成後會進行緊縮),而非活躍對象佔用的空間將會被釋放。完成複製後,From空間和To空間的角色發生對換。也就是說,在垃圾回收的過程中,就是通過將存活對象在兩個semispace之間進行復制。可以很容易看出來,使用Cheney算法時,總有一半的內存是空的。但是由於新生代很小,所以浪費的內存空間並不大。而且由於新生代中的對象絕大部分都是非活躍對象,需要複製的活躍對象比例很小,所以其時間效率十分理想。複製的過程採用的是BFS(廣度優先遍歷)的思想,從根對象出發,廣度優先遍歷所有能到達的對象

具體的執行過程大致是這樣:

首先將From空間中所有能從根對象到達的對象複製到To區,然後維護兩個To區的指針scanPtr和allocationPtr,分別指向即將掃描的活躍對象和即將為新對象分配內存的地方,開始循環。循環的每一輪會查找當前scanPtr所指向的對象,確定對象內部的每個指針指向哪裡。如果指向老生代我們就不必考慮它了。如果指向From區,我們就需要把這個所指向的對象從From區複製到To區,具體複製的位置就是allocationPtr所指向的位置。複製完成後將scanPtr所指對象內的指針修改為新複製對象存放的地址,並移動allocationPtr。如果一個對象內部的所有指針都被處理完,scanPtr就會向前移動,進入下一個循環。若scanPtr和allocationPtr相遇,則說明所有的對象都已被複制完,From區剩下的都可以被視為垃圾,可以進行清理了

寫屏障

如果新生代中的一個對象只有一個指向它的指針 而這個指針在老生代中 我們如何判斷這個新生代的對象是否存活? 為了解決這個問題 我們需要建立一個列表用來記錄所有老生代對象指向新生代對象的情況 每當有老生代對象指向新生代對象的時候 我們記錄下來

對象的晉升

當一個對象經過多次新生代的清理依舊倖存,這說明它的生存週期較長,也就會被移動到老生代,這稱為對象的晉升。具體移動的標準有兩種:

  1. 對象從From空間複製到To空間時,會檢查它的內存地址來判斷這個對象是否已經經歷過一個新生代的清理,如果是,則複製到老生代中,否則複製到To空間中
  2. 對象從From空間複製到To空間時,如果To空間已經被使用了超過25%,那麼這個對象直接被複制到老生代

老生代

老生代所保存的對象大多數是生命週期很長的甚至是常駐內存的對象 而且老生代佔用的內存較多

回收算法

老生代佔用內存較多 如果使用 Scavenge 算法 浪費一半空間 而且耗時較長 故採用其他算法

  • Mark-Sweep(標記清除)
  • 標記清除分為標記和清除兩個階段。在標記階段需要遍歷堆中的所有對象,並標記那些活著的對象,然後進入清除階段。在清除階段總,只清除沒有被標記的對象。由於標記清除只清除死亡對象,而死亡對象在老生代中佔用的比例很小,所以效率較高
  • 標記清除有一個問題就是進行一次標記清楚後,內存空間往往是不連續的,會出現很多的內存碎片。如果後續需要分配一個需要內存空間較多的對象時,如果所有的內存碎片都不夠用,將會使得V8無法完成這次分配,提前觸發垃圾回收。
  • Mark-Compact(標記整理)
  • 標記整理正是為了解決標記清除所帶來的內存碎片的問題。標記整理在標記清除的基礎進行修改,將其的清除階段變為緊縮極端。在整理的過程中,將活著的對象向內存區的一段移動,移動完成後直接清理掉邊界外的內存。緊縮過程涉及對象的移動,所以效率並不是太好,但是能保證不會生成內存碎片

其他

V8後續引入了增量式整理 以及並行標記和並行清理 通過並行利用多核CPU來提升垃圾回收的性能

希望大家能夠轉發點贊,謝謝~

關注作者,我會不定期在頭條分享Java,Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構,BATJ面試 等資料...

轉發此文,關注並私信小編“學習”web前端課程資料馬上免費領取

"

相關推薦

推薦中...