天天看點

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

說明

圖解 Google V8 學習筆記

在 Chrome 中檢視記憶體快照

1、首先我們 f12 在控制台運作下面這段程式

function Student(name,) {
  this.name = name;
  this.gender= gender;
}
var kaimo = new Student('kaimo', '男');      
圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

2、切換到 Memory 中,點選左側的小圓圈就可以捕獲目前的記憶體快照。點開快照後,在過濾器中輸入 Student,即可找到

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

V8 中對象的結構

在 V8 中,對象主要由三個指針構成,分别是​

​隐藏類(Hidden Class)​

​​,​

​Property​

​​ 還有 ​

​Element​

​。

  • 隐藏類用于描述對象的結構。
  • Property 和 Element 用于存放對象的屬性,它們的差別主要展現在鍵名能否被索引。
圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

命名屬性的不同存儲方式

V8 中命名屬性有三種的不同存儲方式:對象内屬性(in-object)、快屬性(fast)和慢屬性(slow)。

  • 對象内屬性儲存在對象本身,提供最快的通路速度。
  • 快屬性比對象内屬性多了一次尋址時間。
  • 慢屬性與前面的兩種屬性相比,會将屬性的完整結構存儲,速度最慢。
圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

隐藏類

上面提到的描述命名屬性是怎麼存放的,在 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__​

​屬性。

  • ​__proto__​

    ​ 屬性就是原型,是用來實作 JavaScript 繼承的。
  • map 則是隐藏類
圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

我們在上面的代碼中再加入一行:

a[1111] = 'aaa'      

打開快照可以看到此時隐藏類發生了變化,Element 中的資料存放也變得沒有規律了。

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

原因:當我們添加了 ​

​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 個屬性,對象内屬性是在對象建立時就固定配置設定的,空間有限,數量固定為十個,空間大小相同(可以了解為十個指針)。

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

快屬性

然後看 b 有 12 個屬性,當對象内屬性放滿之後,會以快屬性的方式,在 ​

​properties​

​​ 下按建立順序存放。相較于對象内屬性,快屬性需要額外多一次 ​

​properties​

​ 的尋址時間,之後便是與對象内屬性一緻的線性查找。

慢屬性

最後看 c 有 30 個屬性,和 b (快屬性)相比,​

​properties​

​ 中的索引變成了毫無規律的數,意味着這個對象已經變成了哈希存取結構了。

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

實踐3:增加屬性對隐藏類的影響

我們先依次執行下面的例子:

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

先執行:

function student(){}
let s = new student()      
圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

再執行:

s.name = 'kaimo'      
圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

最後執行:

s.text = '男'      
圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

我們從上面可以清晰的看到 s 在空對象時、添加 name 屬性後、添加 gender 屬性後會分别對應不同的隐藏類。

大緻示意圖如下:裡面 Offset 不懂的可以參考下面文章:

  • ​​貘吃馍香:V8 Hidden Class​​
  • ​​JavaScript 引擎基礎:Shapes 和 Inline Caches​​

并且我們可以從記憶體快照中看到 Hidden Class2 的 back_pointer 指針指向 Hidden Class1。

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

隐藏類建立時的優化

例子:下面代碼 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'
}      

記憶體快照如下

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

然後執行删除操作:

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 = '男'      

記憶體快照如下:

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

然後再執行删除

delete a.age      
圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

記憶體快照如下:

圖解 Google V8 # 04:V8 中的對象表示:怎麼利用 Chrome 記憶體快照去檢視對象在記憶體中是如何布局的?

為什麼說在 JS 中要避免使用 delete?

  1. delete 很多時候删不掉。
  2. delete 傳回true的時候,也不代表一定删除成功。 比如原型上的屬性。
  3. 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​​
  • ​​【繪圖工具】神繪 | 手繪:虛拟的協作式白闆工具​​

繼續閱讀