内存泄漏

在执行一个长期运行的应用程序时,应用程序分配的内存没有被释放,导致可用内存逐渐减少,最终可能导致浏览器崩溃或者应用性能严重下降的情况,即 JS 内存泄漏

可能导致内存泄漏的场景

  • 不断创建全局变量
  • 未及时清理的闭包:如果闭包中引用了外部的变量,而这个变量又没有被及时释放,就可能发生内存泄漏。
  • DOM元素的引用:如果一个DOM元素被保存在一个JavaScript变量中,但是页面上该DOM元素被移除,这个变量仍然引用着这个DOM元素,导致它无法被垃圾回收。
  • 事件监听器:如果一个DOM元素添加了事件监听器,但在移除该元素之前没有移除事件监听器,则可能发生内存泄漏。
  • 循环引用:两个对象相互引用,导致它们不能被垃圾回收。(现代浏览器改用标记清除的垃圾回收机制,已解决了此问题)

vue 中内存泄漏的场景

组件销毁时未清除全局变量的引用、函数引用、全局事件、自定义事件、定时器。

// 全局变量引用vue实例上的响应式变量
window.list = this.list
// 全局变量引用vue实例上的函数
window.copy = this.copy
  • 1.
  • 2.
  • 3.
  • 4.

如何避免内存泄漏?

  • 避免创建全局变量。
  • 使用严格模式或者let/const声明变量。
  • 及时解除DOM元素的引用。
  • 在移除DOM元素之前,移除相关的事件监听器。
  • 使用弱引用或者引用计数来处理循环引用的问题。(如使用  WeakMap 和WeakSet
  • 使用工具或者浏览器自带的开发者工具检测和分析内存使用情况,找出潜在的内存泄漏。
  • 编写单元测试以确保内存的正确释放。
  • 使用try/catch/finally来确保资源在异常发生时也能被正确释放。
  • 定期进行垃圾回收(例如在JavaScript中可以使用window.gc()来强制进行垃圾回收,但这不是标准方法,并且不建议在生产环境中使用)。

检测 JS 内存泄漏的方法

可使用浏览器自带的开发者工具

JS【详解】内存泄漏(含泄漏场景、避免方案、检测方法),垃圾回收 GC (含引用计数、标记清除、标记整理、分代式垃圾回收)_JS


上图中,HEAP值随时间一直上升,即发生了内存泄漏

正常的内存应该如下图(每隔一段时间,就会进行垃圾回收下降)

JS【详解】内存泄漏(含泄漏场景、避免方案、检测方法),垃圾回收 GC (含引用计数、标记清除、标记整理、分代式垃圾回收)_内存泄漏_02

垃圾回收 GC

GC 是垃圾回收(Garbage Collection)的缩写,是由 JavaScript 引擎自动执行的自动内存管理机制,用于检测和清除不再使用的数据,以释放内存空间。

垃圾回收的目的是减少内存泄漏和提高程序的性能。

JS 垃圾回收的算法

引用计数(之前)

通过跟踪每个对象被引用的次数来确定对象是否为垃圾。

逻辑

  • 当一个对象被创建时,其引用计数器初始化为1。
  • 当该对象被其他对象引用时,引用计数器加1。
  • 当该对象不再被其他对象引用时,引用计数器减1。
  • 当引用计数器减至0时,意味着该对象不再被引用,可以被垃圾收集器回收。
// 创建一个对象
let obj = { name: "test" };
// 创建一个引用指向对象
let ref1 = obj;//引用计数+1 1

// 创建另一个引用指向对象
let ref2 = obj;//引用计数+1 2

// 引用失效
ref1 = null;//引用计数-1 1
ref2 = null;//引用计数-1 0

// 引用计数为0,对象可以被回收
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

优点

  • 实时回收:在对象不再被引用时立即回收,不需要等待垃圾收集器的运行。这可以减少内存占用和提高程序的性能。
  • 简单高效:实现起来相对容易,不需要复杂的算法和数据结构。

缺点

  • 两个或多个对象相互引用时,无法被回收,导致内存泄漏
  • 引用计数需要占用额外的内存空间,而且每次添加、删除引用都需要更新计数,增加了额外的开销。
标记清除(现代)

通过标记不再使用的对象,然后清除这些对象的内存空间,以便后续的内存分配使用。

逻辑

  • 在标记阶段,垃圾回收器会对内存中的所有对象进行遍历,从根对象开始(通常是全局对象)递归地遍历对象的引用关系。对于每个被访问到的对象,垃圾回收器会给它打上标记,表示该对象是可达的,即不是垃圾。
  • 在清除阶段,垃圾回收器会遍历整个内存,对于没有标记的对象,即被判定为垃圾的对象,会被立即回收,释放内存空间。

优点

  • 简单有效:算法相对简单,容易实现
  • 可清除循环引用

缺点

  • 会暂停程序的执行,进行垃圾回收操作。当堆中对象较多时,可能会导致明显的停顿,影响用户体验。
  • 会在回收过程中产生大量的不连续的、碎片化的内存空间。这可能导致后续的内存分配难以找到足够大的连续内存块,从而使得内存的利用率降低。
标记整理(优化)

在标记清除的基础上,增加了整理

逻辑

  • 在标记阶段,标记活动对象
  • 在整理阶段,将内存中的活动对象移动到一端,使得空闲空间连续,并且没有碎片化。
  • 在清除阶段,清除垃圾,回收内存

优点

  • 解决了标记-清除算法产生的碎片化问题,使得内存空间得到更好的利用,减少了空间的浪费。

缺点

  • 会暂停程序的执行,进行垃圾回收操作。当堆中对象较多时,可能会导致明显的停顿,影响用户体验。

V8 引擎的垃圾回收策略

V8是一种由Google开发的用于执行JavaScript 的开源引擎,用于浏览器和Node.js

分代式垃圾回收

将内存划分为新生代(Young Generation)和老生代(Old Generation)两个代:

  • 新生代:存放的是存活时间较短的对象(经过一次垃圾回收后,就被释放回收掉),采用了基于Scavenge算法的快速垃圾回收策略,通过将内存分为两个半空间来进行垃圾回收,优化了对象的分配和回收过程。
  • 老生代:存放的是存活时间较长的对象(经过多次垃圾回收后仍存在),采用了基于标记-整理-清除算法的全垃圾回收策略,通过对整个堆进行标记和整理,以减少内存的碎片化,提高内存利用率。

更多详情可参考
 https://zhuanlan.zhihu.com/p/689678104