前段時間,在排查一個問題的時候,遇到了一個有點令人困惑的情況,有下面這兩段代碼:
const a = new Array(99999);
a[99998] = undefined;
const b = new Array(99999);
b[99999] = undefined;
我們通過
node --inspect-brk
來分别運作這兩段代碼,在代碼運作的最開始和結束的時候分别task heap snapshot,分析對應的記憶體占用資訊如下:

可以發現第二段代碼的記憶體占用明顯要小于第一段,那麼問題就出現在這個
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分别對最開始的兩段代碼進行調試:
可以很明顯的看到,第一個數組為FixedArray,而第二個數組為Dictionary,那麼為什麼隻有第二個數組轉換為了字典模式呢?
在V8中JSArray是繼承于JSObject的,是以當設定屬性的時候,會依次執行
Object::SetProperty
、
Object::AddDataProperty
JSObject::AddDataElement
ShouldConvertToSlowElements
,回到V8代碼中,
ShouldConvertToSlowElements
這個方法,它是用來判斷是否将一個數組轉換為慢模式(Dictionary)(
v8/src/objects/js-objects-inl.h#L794):
從上面的代碼可以看到,當設定
99998
的時候,索引小于目前容量的時候,傳回值為false,也就是不進行轉換。 而當設定
99999
這個索引的值的時候,因為超出了原來的FixedArray容量,那麼就會進行擴容,擴容的算法(
v8/src/objects/js-objects.h#L540)為容量 + 容量 /2 + 16,那麼原來 99999 的容量就會擴容放大到 15萬。
然後會執行
GetFastElementsUsage
來擷取原來的數組中非空洞(
v8/src/objects/js-objects.cc#L4725)的元素數量,乘以
kPreferFastElementsSizeFactor(值為3)
與
kEntrySize (值為2)
,與新的容量長度進行對比,如果小于新的容量長度,那麼就轉換為慢數組。
最開始的第二段代碼中,非空洞元素數量為0,計算後的乘積也為0,是以小于15萬的新數組長度,于是數組轉換為了慢數組,使用了Dictionary進行資料的存儲,進而節省了大量的記憶體。
(本篇内容來自阿裡巴巴淘系技術 洗劍)