1. 开场白:看不见的房间管家
写 JS 的你我,几乎从不手动 malloc/free,但这不代表内存管理"不存在"。
JavaScript 引擎就像一位房间管家:
- 你买东西(创建变量)→ 管家帮你找空位;
- 你用完扔一边(解除引用)→ 管家扫进垃圾桶;
- 你若一直占着茅坑不拉屎(内存泄漏)→ 管家也无可奈何。
理解管家的工作方式,才能让应用不爆内存、不卡顿、不 OOM。
2. 两张图看懂"东西"放哪儿
| 存储区域 | 放什么 | 特点 | 生命周期 |
|---|---|---|---|
| Stack 栈 | 原始值(number、boolean、undefined、null、string、symbol、bigint) |
固定大小、后进先出、超快 | 函数 return 即自动弹栈 |
| Heap 堆 | 引用值(Object、Array、Function、Date、RegExp...) |
动态大小、可按引用共享、稍慢 | 靠垃圾回收器(GC)稍后清理 |
代码示例
js
let age = 18; // 栈
let user = {age}; // 栈里只存指针,实际对象在堆
3. 垃圾回收三部曲
现代引擎默认采用**"标记-清除"(Mark-and-Sweep)算法,配合"分代回收"**优化。
3.1 标记阶段(Mark)
从一组**根(Roots)**出发:
- 当前调用栈里的局部变量;
- 全局对象(
window、globalThis); - 被 Chrome DevTools 断点 Hold 住的变量...
把所有能访问到的对象打上 "在用" 标签,其余都是 "垃圾"。
3.2 清除阶段(Sweep)
线性扫堆,把没标签的对象直接释放;
内存空隙由空闲链表 或按页压缩整理,避免碎片化。
3.3 分代优化(Generational GC)
V8 把堆再细分为:
- 新生代 (1~8 M):短命对象,采用 Scavenge(复制回收),频率高、停顿短;
- 老生代 :多次幸存的大对象、长寿命对象,采用 Mark-Sweep + Mark-Compact,频率低、停顿长。
实测:95% 的对象 20ms 内就死,分代后 GC 吞吐量提升 5~10 倍。
4. 四种常见内存泄漏与排查清单
| 泄漏场景 | 代码味道 | 修复要点 |
|---|---|---|
| 1. 意外全局变量 | function f(){ leaky="oops" } |
'use strict' + ESLint no-undef |
| 2. 忘记清理定时器 | setInterval(()=>{...},1000) |
const t=setInterval(...); 卸载时 clearInterval(t) |
| 3. 闭包捕"大"不放 | return ()=>console.log('hi') 却捕获了 new Array(1e6) |
只把必要变量闭进去,或手动 largeData=null |
| 4. 游离 DOM + 监听器 | removeChild(node) 后仍保留 node 引用 |
node.remove(); node=null; 同时 off() 监听器 |
5. 开发者必备:Chrome DevTools 三板斧
- Performance Monitor
实时折线图:JS heap size、DOM 节点、事件监听器数量。 - Heap Snapshot
对比两次快照,按 "Retained Size" 排序,轻松找出"大胃王"。 - Allocation Timeline
录制 30 秒用户操作,蓝色竖线表示新生代分配,峰值即潜在泄漏点。
6. 进阶:WeakMap / WeakRef 让 GC 更聪明
WeakMap只保存弱引用,键对象被回收时,映射条目自动消失;WeakRef允许你观察对象是否已被 GC,而不阻止其被回收;- 适合缓存、DOM 元数据映射,不增加额外可达路径。
7. 一句话总结
JavaScript 帮你扫地,但别把垃圾藏到床底 ------
理解栈/堆、标记-清除、分代模型,善用 DevTools,远离四种泄漏,
你的页面才能常驻 60 FPS、不爆 256 M 低端机。
8. 延伸阅读
- V8 官方博客:Trash Talk: The Orinoco Garbage Collector
- MDN:Memory Management
- 书籍:《深入浅出 V8》 ------ 字节跳动工程师出品,GC 章节图解详尽。