天天看點

從V8源碼分析一個JS 數組的記憶體占用問題

前段時間,在排查一個問題的時候,遇到了一個有點令人困惑的情況,有下面這兩段代碼:

const a = new Array(99999);
a[99998] = undefined;
const b = new Array(99999);
b[99999] = undefined;      

我們通過

node --inspect-brk

來分别運作這兩段代碼,在代碼運作的最開始和結束的時候分别task heap snapshot,分析對應的記憶體占用資訊如下:

從V8源碼分析一個JS 數組的記憶體占用問題
從V8源碼分析一個JS 數組的記憶體占用問題

可以發現第二段代碼的記憶體占用明顯要小于第一段,那麼問題就出現在這個

99999

的越界指派上面。

在V8代碼(

v8/src/objects/js-array.h#L19

)中有很明确的标注,數組有兩種模式,快數組和慢數組,在數組初始化時,預設的存儲方式為快數組(

v8/src/objects/js-objects.h#L317

),其記憶體占用是連續的,而慢數組會使用HashTable來進行資料存儲。 另外數組會分為壓緊(Packed)的和有洞的(Holey)兩種,例如

['a', 'b', 'c']

這樣的數組長度為3,數組索引0、1、2均有值,那麼就認為是Packed;而對于

['a',,,'d']

這樣的數組,長度為4,但是索引1、2位置并沒有進行初始化指派,那麼就認為是Holey。當數組出現了較大空洞的時候,記憶體明顯是被浪費了。

V8中對于大型空洞數組進行了優化,在V8部落格(

https://v8.dev/blog/fast-properties

)中進行說明了這一點,對于非常大的Holey數組來說,FixedArray會造成記憶體浪費,是以會使用字典來節約記憶體,也就是會使用慢數組模式。

使用v8-debug分别對最開始的兩段代碼進行調試:

從V8源碼分析一個JS 數組的記憶體占用問題
從V8源碼分析一個JS 數組的記憶體占用問題

可以很明顯的看到,第一個數組為FixedArray,而第二個數組為Dictionary,那麼為什麼隻有第二個數組轉換為了字典模式呢?

在V8中JSArray是繼承于JSObject的,是以當設定屬性的時候,會依次執行

Object::SetProperty

Object::AddDataProperty

JSObject::AddDataElement

ShouldConvertToSlowElements

,回到V8代碼中,

ShouldConvertToSlowElements

這個方法,它是用來判斷是否将一個數組轉換為慢模式(Dictionary)(

v8/src/objects/js-objects-inl.h#L794

):

從V8源碼分析一個JS 數組的記憶體占用問題

從上面的代碼可以看到,當設定

99998

的時候,索引小于目前容量的時候,傳回值為false,也就是不進行轉換。 而當設定

99999

這個索引的值的時候,因為超出了原來的FixedArray容量,那麼就會進行擴容,擴容的算法(

v8/src/objects/js-objects.h#L540

)為容量 + 容量 /2 + 16,那麼原來 99999 的容量就會擴容放大到 15萬。

從V8源碼分析一個JS 數組的記憶體占用問題

然後會執行

GetFastElementsUsage

來擷取原來的數組中非空洞(

v8/src/objects/js-objects.cc#L4725

)的元素數量,乘以

kPreferFastElementsSizeFactor(值為3)

kEntrySize (值為2)

,與新的容量長度進行對比,如果小于新的容量長度,那麼就轉換為慢數組。

最開始的第二段代碼中,非空洞元素數量為0,計算後的乘積也為0,是以小于15萬的新數組長度,于是數組轉換為了慢數組,使用了Dictionary進行資料的存儲,進而節省了大量的記憶體。

(本篇内容來自阿裡巴巴淘系技術 洗劍)

繼續閱讀