内存与内存泄漏
内存泄漏是前端最隐蔽的问题之一:它不会立刻报错,而是让页面在长时间运行后越来越卡、越来越占内存,最终崩溃。理解 JS 内存模型与 V8 垃圾回收机制,能在面试中讲清"为什么会泄漏""如何排查",是中高级前端必备能力,对重度交互的 SPA、富文本编辑器、可视化大屏项目尤其重要。
JS 内存模型
JavaScript 的内存空间大致 分为三类:
| 区域 | 存放内容 | 大小 | 回收方式 |
|---|---|---|---|
| 栈(Stack) | 基本类型值(number、string、boolean、null、undefined、symbol)、引用地址 | 小、固定 | 自动随执行上下文弹出 |
| 堆(Heap) | 对象、数组、函数等引用类型 | 大、动态 | GC 管理 |
| 常量池 / 代码段 | 字符串常量、编译后的字节码 | — | — |
// 基本类型:值本身在栈里
let a = 10;
let b = a; // 复制值
b = 20;
console.log(a); // 10
// 引用类型:引用在栈,对象在堆
let obj1 = { x: 1 };
let obj2 = obj1; // 复制引用,指向同一个堆对象
obj2.x = 2;
console.log(obj1.x); // 2
V8 垃圾回收
V8(Chrome/Node 的 JS 引擎)采用分代回收策略,把堆分为新生代与老生代,分别用不同算法。
新生代(Young Generation)
- 存放:刚创建的对象
- 大小:较小(通常 1–8 MB)
- 算法:Scavenge(半空间复制)
Scavenge 把堆分为两个半空间(From / To):
1. 新对象分配在 From 空间
2. GC 时,遍历 From 中的存活对象,复制到 To 空间(紧凑排列)
3. 清空 From
4. From 与 To 角色互换
特点:
- 速度快,适合"朝生夕死"的短命对象
- 牺牲空间换时间(一半空间闲置)
- 存活过一次 GC 的对象晋升到老生代
老生代(Old Generation)
- 存放:存活时间较长、晋升过来的对象
- 大小:较大(数百 MB 甚至 GB)
- 算法:标记-清除(Mark-Sweep)+ 标记-整理(Mark-Compact)
Mark-Sweep(标记清除):
- 从根(GC Root)出发,递归遍历所有可达对象,标记为"存活"
- 清除未被标记的对象(不可达)
Mark-Compact(标记整理):
- Mark-Sweep 会产生内存碎片
- Mark-Compact 在清除时把存活对象移动到一端,整理出连续空闲空间
新生代与老生代的差异
| 维度 | 新生代 | 老生代 |
|---|---|---|
| 大小 | 小(MB) | 大(GB) |
| 算法 | Scavenge | Mark-Sweep + Mark-Compact |
| 速度 | 快 | 较慢 |
| 频率 | 高 | 低 |
| 暂停 | 短 | 较长(增量、并发优化) |
V8 的优化技术
- 增量标记(Incremental Marking):把长 GC 拆成多次小步,与 JS 交替执行,减少卡顿
- 并发标记(Concurrent Marking):标记阶段在辅助线程执行,不阻塞主线程
- Orinoco:V8 现代 GC 的总称,目标是把全停顿(Stop-The-World)降到最低
GC Root
GC 从"根"出发判断可达性。前端的 GC Root 主要包括:
- 全局对象(
window/globalThis) - 当前执行栈中的局部变量
- 闭包引用的变量
- DOM 树中的节点(被引用的)
- 定时器回调引用的变量
只要对象被 GC Root 直接或间接引用,就不会被回收——这是内存泄漏的根本原因。