JavaScript内存管理依赖自动垃圾回收(GC),堆中引用类型需GC清理,栈中基本类型自动释放;标记-清除算法可处理循环引用,而引用计数已弃用;泄漏主因是未清除监听器、定时器或闭包引用,需通过Memory面板比对堆快照定位。

JavaScript 内存管理靠自动分配 + 自动回收,你不用 malloc 也不用 free,但这也意味着——写错一句引用,内存就悄悄涨上去不回落。
它不是“完全不管”,而是把责任交给了引擎的垃圾回收器(GC),而 GC 能否正确识别“该回收谁”,取决于你写的代码是否切断了所有可达路径。
栈内存和堆内存分工明确,但只有堆才需要 GC
基本类型(number、string、boolean、undefined、null、symbol、bigint)存在栈中,函数退出时自动弹出,无需 GC 干预。
而对象、数组、函数等引用类型,实际数据存在堆里,栈里只存一个指向它的地址。只要还有至少一个变量(或闭包、事件监听器、定时器回调等)能顺着这条地址“摸到”它,它就**不可回收**。
立即学习“Java免费学习笔记(深入)”;
- ✅ 正常:函数返回后,局部对象没被外部保留 → 引擎下次 GC 就清掉
- ❌ 常见泄漏:给
document绑了事件监听器却没removeEventListener→ 对象一直被 DOM 树引用 - ❌ 隐蔽泄漏:
setTimeout回调里闭包捕获了大对象 → 即使组件卸载,回调未清除,对象仍存活
主流 GC 算法是标记-清除,不是引用计数
现代 JS 引擎(V8、SpiderMonkey、JavaScriptCore)都用 标记-清除(Mark-and-Sweep),不是引用计数。
引用计数曾因循环引用问题被弃用:两个对象互相赋值给对方属性,引用数永远 ≥1,即使它们已脱离业务逻辑,也无法回收。
function leak() {
const objA = {};
const objB = {};
objA.ref = objB;
objB.ref = objA;
// 函数执行完,objA 和 objB 仍互相引用 → 引用计数算法下永不释放
}标记-清除则不管这个:从全局对象、当前执行上下文等“根”出发,顺着所有引用链走一遍,能走到的就是“活着的”;走不到的,哪怕有循环,也照删不误。
你没法手动触发 GC,但可以影响它的效率
没有标准 API 让你调用 gc()(Chrome DevTools 的 console.memory.gc() 仅限调试,生产环境禁用)。
你能做的,是减少 GC 工作量:
- 避免在循环中频繁创建新对象,改用复用或池化(如
const pool = []+pool.pop() || new HeavyObject()) - 及时切断引用:将不再需要的大对象变量设为
null,尤其在组件unmount或destroy钩子中 - 清理定时器:
clearTimeout/clearInterval必须配对使用,否则闭包保活风险极高 - 移除事件监听器:用命名函数而非匿名函数注册,确保能精准
removeEventListener
内存泄漏的真实信号不是“卡”,而是“越用越多”
DevTools 的 MutationObserver 或 Performance 面板看不出泄漏;得看 Memory 面板里的堆快照(Heap Snapshot)对比:
- 两次操作后拍快照 → 搜索关键词(如组件名、构造函数名)→ 看实例数是否持续增长
- 筛选
Detached DOM tree→ 找出被 JS 引用但已从 DOM 移除的节点 - 注意
Closure下挂载的变量:它可能正偷偷留住一个 10MB 的图片数组
最麻烦的泄漏往往不报错、不崩溃,只是用户多刷几次列表页,内存占用从 50MB 涨到 800MB —— 直到浏览器主动 kill 掉 tab。











