从V8源码分析一个JS 数组的内存占用问题

简介: 前段时间,在排查一个问题的时候,遇到了一个有点令人困惑的情况,有下面这两段代码:

前段时间,在排查一个问题的时候,遇到了一个有点令人困惑的情况,有下面这两段代码:

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

我们通过 node --inspect-brk 来分别运行这两段代码,在代码运行的最开始和结束的时候分别task heap snapshot,分析对应的内存占用信息如下:

image.pngimage.png


可以发现第二段代码的内存占用明显要小于第一段,那么问题就出现在这个 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分别对最开始的两段代码进行调试:

image.png

image.png

可以很明显的看到,第一个数组为FixedArray,而第二个数组为Dictionary,那么为什么只有第二个数组转换为了字典模式呢?

在V8中JSArray是继承于JSObject的,所以当设置属性的时候,会依次执行 Object::SetPropertyObject::AddDataPropertyJSObject::AddDataElementShouldConvertToSlowElements ,回到V8代码中,ShouldConvertToSlowElements这个方法,它是用来判断是否将一个数组转换为慢模式(Dictionary)(v8/src/objects/js-objects-inl.h#L794):

image.png

从上面的代码可以看到,当设置 99998 的时候,索引小于当前容量的时候,返回值为false,也就是不进行转换。 而当设置 99999 这个索引的值的时候,因为超出了原来的FixedArray容量,那么就会进行扩容,扩容的算法(v8/src/objects/js-objects.h#L540)为容量 + 容量 /2 + 16,那么原来 99999 的容量就会扩容放大到 15万。

image.png


然后会执行 GetFastElementsUsage 来获取原来的数组中非空洞(v8/src/objects/js-objects.cc#L4725)的元素数量,乘以 kPreferFastElementsSizeFactor(值为3)kEntrySize (值为2) ,与新的容量长度进行对比,如果小于新的容量长度,那么就转换为慢数组。


最开始的第二段代码中,非空洞元素数量为0,计算后的乘积也为0,因此小于15万的新数组长度,于是数组转换为了慢数组,使用了Dictionary进行数据的存储,从而节省了大量的内存。

(本篇内容来自阿里巴巴淘系技术 洗剑)

相关文章
|
8天前
|
JavaScript 前端开发 索引
JavaScript中的数组的内置方法全面讲解
JavaScript 数组提供了多种内置方法来高效操作数据。如 `push()` 和 `unshift()` 分别在数组尾部和头部添加元素;`pop()` 和 `shift()` 则移除尾部和头部的元素;`splice()` 可增删元素;`slice()` 创建子数组;`join()` 将数组转化为字符串;`indexOf()` 和 `includes()` 用于查找元素;`forEach()` 遍历数组。此外,`reverse()` 和 `sort()` 改变数组顺序;`fill()` 填充数组值;`slice()` 和 `concat()` 则分别用于创建子数组和合并数组。
19 2
|
8天前
|
JavaScript 前端开发 API
复习JavaScript中的数组API
这段代码介绍了JavaScript数组的几种实用方法:`fill`用于用固定值覆盖指定范围内的元素;`find`查找符合条件的第一个元素;`findIndex`获取该元素的索引;`sort`对数组排序,需提供比较函数以正确排序数字;结合`slice`与`sort`可在不改动原数组的情况下进行排序;`reverse`反转数组元素;利用`map`可创建新数组并替换其中的特定元素,同时保持原数组不变。
8 1
|
5天前
|
存储 JavaScript 算法
|
5天前
|
JavaScript 前端开发
JavaScript基础&实战(5)js中的数组、forEach遍历、Date对象、Math、String对象
这篇文章介绍了JavaScript中的数组、Date对象、Math对象以及包装类(String、Number、Boolean),并详细讲解了数组的创建、方法(如forEach、push、pop、unshift、slice、splice)和遍历操作,以及工厂方法创建对象和原型对象的概念。
JavaScript基础&实战(5)js中的数组、forEach遍历、Date对象、Math、String对象
|
5天前
|
JavaScript 前端开发 索引
JavaScript数组相关的方法有哪些?
JavaScript数组相关的方法有哪些?
|
5天前
|
JavaScript 前端开发
记录Javascript数组类练习
记录Javascript数组类练习
|
5天前
|
JavaScript 容器
JS-数组的定义
JS-数组的定义
|
6天前
|
JavaScript 前端开发
JavaScript——快速判断数组对象的值是否全部满足条件
JavaScript——快速判断数组对象的值是否全部满足条件
19 0
|
7天前
|
JavaScript 前端开发 索引
JavaScript数组的常用方法
JavaScript数组的常用方法
13 0
|
8天前
|
JavaScript
js如何添加新元素到数组中
js如何添加新元素到数组中
18 0

热门文章

最新文章