javascript的垃圾回收如何工作_如何避免内存泄漏

13次阅读

javaScript垃圾回收采用标记-清除机制,从根对象出发标记可达对象,未标记者被清除;常见泄漏源包括未解绑事件监听器、未清除定时器、闭包过度捕获及缓存未释放,需用chrome Memory面板分析快照定位悬空引用。

javascript的垃圾回收如何工作_如何避免内存泄漏

javascript 的垃圾回收不靠你手动 deleteNULL,而靠引擎自动判断“这个对象还能不能被访问到”。只要它还能从根(比如 window、当前函数的局部变量)顺着引用链摸到,就不会被回收——哪怕你已经把它设为 null,只要别的地方还留着引用,它就照活不误。

标记清除是核心机制,不是引用计数

现代引擎(V8、SpiderMonkey)全用标记-清除(Mark-and-Sweep),不是引用计数。后者早被淘汰,因为它搞不定循环引用:两个对象互相 a.ref = b; b.ref = a,但外部没人再用它们,引用计数永远是 1,内存就卡死不放。

标记清除分两步:

  • 标记阶段:从根出发,递归遍历所有能触达的对象,打上“活跃”标记
  • 清除阶段:把没被标记的对象整块内存释放

这个过程不是实时的,而是在空闲或内存压力大时触发,所以你看到内存曲线有延迟回落,属正常。

立即学习Java免费学习笔记(深入)”;

事件监听器没配对移除,dom 就悬在半空

这是最常见泄漏点:元素已从 DOM 中 removeChild 或被框架卸载,但 addEventListener 绑定的监听器还在,尤其用匿名函数或闭包时,整个作用域链都被锁住。

典型错误写法:

element.addEventListener('click', () => {   console.log(this.data); // 闭包捕获了 this 和大量数据 });

正确做法:

  • 用具名函数 + removeEventListener 配对清理
  • 组件级开发中,在 useEffect 返回函数(react)或 beforeUnmountvue)里统一解绑
  • 优先用 { once: true } 选项,适合只触发一次的场景
  • 对动态节点,改用事件委托,避免逐个绑定

定时器和闭包联手“绑架” this 和大数组

setInterval 回调里用了 this.statelargeList,组件卸载后定时器还在跑?那整个组件实例和它闭包里的所有数据都动不了。

同样,循环中定义函数也危险:

for (let i = 0; i < 1000; i++) {   elements[i].addEventListener('click', () => {     console.log(dataList); // 每次都捕获整个 dataList,不是 item.id   }); }

结果:1000 个闭包全锁住 dataList,它永远不可达不了。

改进方式:

  • 组件销毁前必须调用 clearInterval(id)clearTimeout(id)
  • 闭包里只取真正需要的值,比如 const id = item.id; handler = () => console.log(id)
  • 大型缓存对象不用时,显式设为 cache = null,别只清空数组 arr.Length = 0
  • 考虑用 WeakMap 存元数据:键是 DOM 元素,值不会阻止元素被回收

排查泄漏不能靠猜,得看堆快照

光靠代码检查容易漏,必须用工具验证。chrome devtoolsMemory 面板 是主力:

  • 操作前拍一个堆快照(Heap Snapshot),操作后(比如跳转页面、关闭弹窗)再拍一个,用 Comparison 视图对比
  • 重点筛:Detached DOM tree(游离 DOM)、重复增长的 Array / Object / 自定义类实例
  • 点开可疑对象,看右侧 Retainers 标签,顺引用链往上找谁在拽着它不放
  • 配合 Allocation Timeline 录制,观察某段 js 执行是否持续分配却不释放

真正难的不是理解 GC 原理,而是发现那些“看起来已经删了,其实还被某个闭包、定时器、全局属性悄悄攥着”的引用链。

text=ZqhQzanResources