我不明白为什么堆大小是应有的两倍。
我创建了一个完美的二叉树。我猜 v8 知道每个节点有 3 个字段。
function buildTree(depth) {
if (depth === 0) return null;
return {
value: 0,
left: buildTree(depth - 1),
right: buildTree(depth - 1),
};
}
我假设 1 个数字占用 8 个字节,2 个对象引用各占用 8 个字节。因此,一个树节点总共需要 24 个字节。
然后我运行下面的代码:
const tree = buildTree(25);
// 2 ** 25 - 1 ≈ 33_500_000 nodes
// 33_500_000 nodes × 24 bytes = 840_000_000 bytes
const { heapUsed } = process.memoryUsage();
const expectedSize = (N * 24 / 1e6).toFixed(2) + " MB";
const actualSize = (heapUsed / 1e6).toFixed(2) + " MB";
console.table({ expectedSize, actualSize });
// ┌──────────────┬──────────────┐
// │ expectedSize │ '805.31 MB' │
// │ actualSize │ '1614.08 MB' │
// └──────────────┴──────────────┘
然后我尝试在 Chrome 中运行这段代码。我只是打开 devtools,在“控制台”选项卡中运行代码,然后切换到“内存”选项卡,如我所料看到 807 MB。
v8 是如何工作的?为什么需要2倍的内存?与 Chrome 相比有什么区别?
(这里是 V8 开发人员。我重新提出了这个问题,因为关键点“为什么这个对象树在 Node 中需要的内存是 Chrome 中的两倍?”,链接的问题尚未回答。)
这有两个部分。
第 1 部分:对象布局。 JavaScript 等语言的虚拟机中的对象具有内部对象标头。这里跳过细节,结果是在 V8 中,每个 JS 对象在其内部标头中有 3 个指针。您的示例创建的特定对象还需要另外 3 个指针来存储其属性(
value
、left
、right
),每个对象总共需要 6 个指针。
第 2 部分:指针压缩。 V8 在 64 位平台上支持“指针压缩”,其中每个堆上指针仅使用 32 位(即指针及其目标都驻留在垃圾收集堆上)。该技术有两个明显的含义:
(1) 它将垃圾收集堆的可寻址大小限制为 2**32 == 4 GiB。
(2) 它将指针的内存消耗减少了一半。字符串、数字等数据的内存消耗不受影响;实际上,这意味着应用程序的总体内存消耗通常会减少三分之一左右。
现在我们可以解释发生了什么:
在 Chrome 中,启用了指针压缩,因此每个 6 字段对象需要
6*4
字节,因此您的示例总共需要 2**25 * 6*4
= 8.05 亿字节。6*8
字节,所以你的示例总共需要 2**25 * 6*8
= 16 亿字节。
附注一个额外的细节:本例中对象中存储的数字实际上始终是相同的数字 0,因此 V8 使用其指针标记技术将其存储为某种“假指针”。这既不是 64 位 IEEE 格式,也不是单独的对象;如果您想了解更多信息,请查找术语“smi tagging”。如果您修改示例以将
value: Math.random()
存储在对象中,您会发现 Chrome 中每个对象的内存消耗增加 12 字节,Node 中每个对象的内存消耗增加 16 字节。