說明
圖解 Google V8 學習筆記
在 Chrome 中檢視記憶體快照
1、首先我們 f12 在控制台運作下面這段程式
function Student(name,) {
this.name = name;
this.gender= gender;
}
var kaimo = new Student('kaimo', '男');

2、切換到 Memory 中,點選左側的小圓圈就可以捕獲目前的記憶體快照。點開快照後,在過濾器中輸入 Student,即可找到
V8 中對象的結構
在 V8 中,對象主要由三個指針構成,分别是
隐藏類(Hidden Class)
,
Property
還有
Element
。
- 隐藏類用于描述對象的結構。
- Property 和 Element 用于存放對象的屬性,它們的差別主要展現在鍵名能否被索引。
命名屬性的不同存儲方式
V8 中命名屬性有三種的不同存儲方式:對象内屬性(in-object)、快屬性(fast)和慢屬性(slow)。
- 對象内屬性儲存在對象本身,提供最快的通路速度。
- 快屬性比對象内屬性多了一次尋址時間。
- 慢屬性與前面的兩種屬性相比,會将屬性的完整結構存儲,速度最慢。
隐藏類
上面提到的描述命名屬性是怎麼存放的,在 V8 中被稱為 Map,更出名的稱呼是
隐藏類(Hidden Class)
。
在 SpiderMonkey (火狐引擎)中,類似的設計被稱為 Shape
。
為什麼要引入隐藏類?
1、進行通路更快:
通過哈希表的方式存取屬性,需要額外的哈希計算。為了提高對象屬性的通路速度,實作對象屬性的快速存取,V8 中引入了隐藏類。
2、節省了記憶體空間:
在 ECMAScript 中,對象屬性的
Attribute
被描述為以下結構。
-
:屬性的值[[Value]]
-
:定義屬性是否可寫(即是否能被重新配置設定)[[Writable]]
-
:定義屬性是否可枚舉[[Enumerable]]
-
:定義屬性是否可配置(删除)[[Configurable]]
因為一般情況下,對象的 Value 是經常會發生變動的,而 Attribute 是幾乎不怎麼會變的。隐藏類的引入,将屬性的 Value 與其它 Attribute 分開。這樣的話可以減少記憶體的浪費。
隐藏類的建立
對象建立過程中,每添加一個命名屬性,都會對應一個生成一個新的隐藏類。在 V8 的底層實作了一個将隐藏類連接配接起來的轉換樹,如果以相同的順序添加相同的屬性,轉換樹會保證最後得到相同的隐藏類。
具體可以看下面【實踐3】的例子。
實踐1:可索引屬性和命名屬性的存放
我們先去控制台執行下面代碼
function Foo1 () {}
var a = new Foo1()
var b = new Foo1()
a.name = 'aaa'
a.text = 'aaa'
b.name = 'bbb'
b.text = 'bbb'
a[1] = 'aaa'
a[2] = 'aaa'
上面代碼中,a、b 都有命名屬性 name 和 text,另外 a 還額外多了兩個可索引屬性。
打開快照可以明顯的看到,可索引屬性是存放在 elements 中的。
V8 還為每個對象實作了 map 屬性和
__proto__
屬性。
-
屬性就是原型,是用來實作 JavaScript 繼承的。__proto__
- map 則是隐藏類
我們在上面的代碼中再加入一行:
a[1111] = 'aaa'
打開快照可以看到此時隐藏類發生了變化,Element 中的資料存放也變得沒有規律了。
原因:當我們添加了
a[1111]
之後,數組會變成稀疏數組。為了節省空間,稀疏數組會轉換為哈希存儲的方式,而不再是用一個完整的數組描述這塊空間的存儲。
哈希存儲亦稱“散列存儲”,專用于集合結構的一種存儲方式。
資料元素存放在一塊連續的存儲區域中。資料元素的存放位置是通過一個哈希函數計算而得的。哈希函數将資料元素作為自變量,計算得到的函數值是資料元素的存儲位址。
實踐2:三種不同類型的 Property 存儲模式
我們先去控制台執行下面代碼
function Foo2() {}
var a = new Foo2()
var b = new Foo2()
var c = new Foo2()
for (var i = 0; i < 10; i ++) {
a[new Array(i+2).join('a')] = 'aaa'
}
for (var i = 0; i < 12; i ++) {
b[new Array(i+2).join('b')] = 'bbb'
}
for (var i = 0; i < 30; i ++) {
c[new Array(i+2).join('c')] = 'ccc'
}
a、b 和 c 分别擁有 10 個,12 個和 30 個屬性
對象内屬性
先看 a 有10 個屬性,對象内屬性是在對象建立時就固定配置設定的,空間有限,數量固定為十個,空間大小相同(可以了解為十個指針)。
快屬性
然後看 b 有 12 個屬性,當對象内屬性放滿之後,會以快屬性的方式,在
properties
下按建立順序存放。相較于對象内屬性,快屬性需要額外多一次
properties
的尋址時間,之後便是與對象内屬性一緻的線性查找。
慢屬性
最後看 c 有 30 個屬性,和 b (快屬性)相比,
properties
中的索引變成了毫無規律的數,意味着這個對象已經變成了哈希存取結構了。
實踐3:增加屬性對隐藏類的影響
我們先依次執行下面的例子:
先執行:
function student(){}
let s = new student()
再執行:
s.name = 'kaimo'
最後執行:
s.text = '男'
我們從上面可以清晰的看到 s 在空對象時、添加 name 屬性後、添加 gender 屬性後會分别對應不同的隐藏類。
大緻示意圖如下:裡面 Offset 不懂的可以參考下面文章:
- 貘吃馍香:V8 Hidden Class
- JavaScript 引擎基礎:Shapes 和 Inline Caches
并且我們可以從記憶體快照中看到 Hidden Class2 的 back_pointer 指針指向 Hidden Class1。
隐藏類建立時的優化
例子:下面代碼 a 和 b 的差別是,a 首先建立一個空對象,然後給這個對象新增一個命名屬性 name。而 b 中直接建立了一個含有命名屬性 name 的對象。
let a = {};
a.name = 'kaimo'
let b = { name: 'kaimo313' }
a 和 b 的隐藏類不一樣,back_pointer 也不一樣。因為在建立 b 的隐藏類時,省略了為空對象單獨建立隐藏類的一步。
要生成相同的隐藏類,更為準确的描述是 —— 從相同的起點,以相同的順序,添加結構相同的屬性(除 Value 外,屬性的 Attribute 一緻)。
實踐4:delete 操作對隐藏類的影響
按照添加屬性的順序删除屬性
現在控制台執行下面代碼:
function Foo4 () {}
var a = new Foo4()
var b = new Foo4()
for (var i = 1; i < 8; i ++) {
a[new Array(i+1).join('a')] = 'aaa'
b[new Array(i+1).join('b')] = 'bbb'
}
記憶體快照如下
然後執行删除操作:
delete a.a
檢視記憶體快照如下:
删除了
a.a
後,a 變成了慢屬性,退回哈希存儲了。
按照添加屬性的順序逆向删除屬性
在控制台執行下面代碼
function Foo5() {}
var a = new Foo5()
var b = new Foo5()
a.name = 'kaimo'
a.gender = '男'
a.age = '8'
b.name = 'kaimo'
b.gender = '男'
記憶體快照如下:
然後再執行删除
delete a.age
記憶體快照如下:
為什麼說在 JS 中要避免使用 delete?
- delete 很多時候删不掉。
- delete 傳回true的時候,也不代表一定删除成功。 比如原型上的屬性。
- delete 某些場景下會導緻隐藏類改變,可能導緻性能問題。
拓展資料
- V8 是怎麼跑起來的 —— V8 中的對象表示
- JavaScript 引擎基礎:Shapes 和 Inline Caches
- V8 Hidden Class
- 貘吃馍香:V8 Hidden Class
- engineering:v8-hidden-class
- 漫畫:什麼是HashMap?
- v8.dev 裡面的文章
- JavaScript中delete操作符不能删除的對象
- 在 JS 中要盡量避免使用 delete 操作符
- 謹慎使用delete
- 為什麼說在 JS 中要避免使用 delete
- 【繪圖工具】神繪 | 手繪:虛拟的協作式白闆工具