{T}

栈空间与堆空间:JavaScript 的内存模型

概述

JavaScript 引擎将内存划分为栈(Stack)和堆(Heap)两个区域,分别管理不同类型的数据。理解数据的存储位置和生命周期,是分析内存分配、垃圾回收和性能优化的前提。


1 栈空间(Stack)

1.1 栈的特性

特性说明
大小固定(V8 默认约 984 KB 用于 JS 栈帧)
分配方式自动分配和释放(随函数调用/返回)
访问速度极快(CPU 缓存友好,L1 缓存命中率极高)
生命周期与函数调用同步(函数返回即释放)

1.2 栈中存储的数据

栈存储原始类型(Primitive)的值和引用类型的地址:

类型存储位置示例
Number栈(Smi 或 HeapNumber)let x = 42;
String栈存引用,堆存内容let s = "hello";
Booleanlet b = true;
undefinedlet u = undefined;
nulllet n = null;
Symbol栈存引用let sym = Symbol();
BigInt栈存引用let big = 9007199254740991n;
Object栈存引用(堆地址)let obj = { x: 1 };

1.3 V8 的 Smi 优化

V8 对小整数(Small Integer, Smi)进行特殊优化:31 位有符号整数直接存储在栈帧中,无需堆分配:

code
Smi 编码(64 位平台):
┌────────────────────────────────┬───┐
│       32 位整数值               │ 0 │  ← 最低位为 0 标识 Smi
└────────────────────────────────┴───┘

HeapNumber 编码:
┌────────────────────────────────┬───┐
│       堆地址(指向 HeapNumber)   │ 1 │  ← 最低位为 1 标识堆对象
└────────────────────────────────┴───┘

Smi 范围:[-2³⁰, 2³⁰ - 1](即 -1073741824 到 1073741823),超出此范围的数字使用 HeapNumber 存储在堆中。


2 堆空间(Heap)

2.1 堆的特性

特性说明
大小动态增长(受系统内存限制)
分配方式手动/自动(由 GC 管理)
访问速度较慢(需间接寻址,缓存命中率低)
生命周期由垃圾回收器管理

2.2 堆中存储的数据

所有引用类型的实际数据存储在堆中:

  • 对象(Object、Array、Map、Set 等)
  • 函数(Function 对象)
  • 字符串(长字符串)
  • HeapNumber(超出 Smi 范围的数字)
  • 闭包捕获的变量环境

2.3 V8 堆的分代结构

图表渲染中…

3 原始类型 vs 引用类型

3.1 赋值行为差异

javascript
// 原始类型:值拷贝
let a = 1;
let b = a;   // b 获得值 1 的独立副本
b = 2;
console.log(a); // 1(a 不受影响)

// 引用类型:引用拷贝
let obj1 = { x: 1 };
let obj2 = obj1; // obj2 获得同一对象的引用
obj2.x = 2;
console.log(obj1.x); // 2(obj1 和 obj2 指向同一对象)
图表渲染中…

3.2 比较行为差异

javascript
// 原始类型:值比较
1 === 1;          // true
"hello" === "hello"; // true

// 引用类型:引用比较
{} === {};        // false(不同对象,不同引用)
const obj = {};
obj === obj;      // true(同一对象,同一引用)

3.3 函数传参行为

JavaScript 采用**值传递(Pass by Value)**策略:函数参数获得实参的副本。对于引用类型,副本是引用地址:

javascript
function mutate(obj, num) {
    obj.x = 10;   // 修改堆中对象(外部可见)
    num = 10;     // 修改栈中副本(外部不可见)
}

const myObj = { x: 1 };
let myNum = 1;
mutate(myObj, myNum);
console.log(myObj.x); // 10(对象被修改)
console.log(myNum);    // 1(原始值未变)

4 内存与性能

4.1 栈 vs 堆的访问性能

操作
分配O(1)(ESP 移动)O(?)(需查找空闲块)
访问O(1)(直接偏移)O(1) 基础 + 缓存未命中惩罚
回收O(1)(ESP 回移)GC 管理(标记-清除/整理)

4.2 优化建议

  1. 优先使用原始类型:原始类型在栈中分配,无需 GC 参与
  2. 避免不必要的对象创建:短生命周期对象增加 GC 压力
  3. 使用对象池:对频繁创建/销毁的对象,使用池化复用
  4. 注意闭包的堆分配:闭包捕获的变量存储在堆中,生命周期延长

5 总结

维度栈空间堆空间
存储内容原始类型值、引用地址引用类型的实际数据
大小固定(~1MB)动态增长
分配/释放自动(随函数调用)GC 管理
访问速度
生命周期确定性(函数作用域)不确定性(GC 决定)

理解栈与堆的分工,是分析 JavaScript 内存行为的基础。原始类型的值语义和引用类型的引用语义,决定了赋值、传参和比较的不同行为。V8 的 Smi 优化和分代 GC 进一步细化了内存管理的策略。


参考文献

  1. V8 Blog: Fast properties
  2. V8 Blog: Pointer Compression
  3. MDN: Memory Management