ECMAScript内存管理
JavaScript 内存分配、可达性、垃圾回收与常见泄漏场景。
#type / concept
#status / growing
#resource / javascript
#resource / ecmascript
[!info] related notes
- 所属 MOC: ecmascript-moc, ecmascript
- 相邻主题: ecmascript-collection-reference-types, ecmascript-basic-reference-types
- 常见关联: ecmascript-closures, javascript中的进程线程协程
- Node 内存: [[buffer]], nodejs-heap-memory-overflow-error
ECMAScript内存管理
JavaScript 会自动分配和回收内存,但自动回收不代表代码天然不会泄漏。理解这篇的核心,是抓住“可达性”和“引用生命周期”。
先抓住两个概念
- 垃圾回收(GC): 自动识别并释放不再使用的内存
- 可达性(reachability): 从根对象出发无法再访问到的值,才有机会被回收
这里的根对象通常包括全局对象、当前调用栈上的局部变量、仍被宿主保存的回调引用等。
常见回收思路
标记-清除
- 先从根对象出发标记所有可达对象
- 再回收没有被标记的对象
- 能处理循环引用,是现代引擎的主流思路
引用计数
- 为对象记录被引用次数
- 引用数降到
0时回收 - 直观但难处理循环引用,所以现在更多作为历史对照来理解
现代引擎常见优化
- 分代回收: 把短命对象和长寿对象分开处理
- 增量标记: 把长时间标记拆成多个小步骤,减少卡顿
- 闲时回收: 在较空闲的时间片执行部分 GC 工作
这也是为什么“对象创建很多”不一定立刻出问题,但不必要的长生命周期引用通常更危险。
回收什么时候发生
GC 不是在你调用某个固定 API 时立刻执行,也不是和浏览器绘制同步触发。
- 具体回收时机由 JS 引擎决定
- 通常会避开当前同步执行的关键路径
- 可能在任务切换、空闲时段、或引擎认为合适的时机运行
- 浏览器页面是否 repaint,不等于 GC 是否发生
所以,真正要关注的不是“什么时候手动回收”,而是“有没有不必要地把对象一直保持可达”。
实践里最值得先做的事
- 只保留还需要的数据引用
- 大数组或大对象不再需要时,及时解除引用
- 定时器、事件监听、订阅关系用完就清理
- 循环里反复创建但可复用的函数或对象,尽量复用
- 对缓存和闭包保存的数据保持克制
补充直觉:把变量设为 null 或让其离开作用域,意义不在“手动释放内存”,而在于切断引用,让 GC 可以判断它是否仍然可达。
常见泄漏场景
- 意外的全局变量
- 被遗忘的
setInterval、回调或订阅 - 已脱离文档但仍被代码引用的 DOM
- 长期持有外部状态的闭包或缓存
内存泄漏和内存溢出的区别
- 内存泄漏: 本来该释放的内存仍被保留
- 内存溢出: 当前需要的内存超过了可用上限
泄漏往往是溢出的诱因之一,但两者不是同一个概念。
延伸阅读
- 看弱引用容器和对象容器:ecmascript-collection-reference-types
- 看闭包为什么会延长变量生命周期:ecmascript-closures
- 看宿主线程和调度背景:javascript中的进程线程协程