# 内存泄漏

没有被垃圾回收且不在用到的对象内存称为内存泄漏(Memory leak)

# 1、常见的内存泄漏

# 1.1 不正当的闭包

闭包的描述:

  • JavaScript高级程序设计:闭包是指有权访问另一个函数作用域中的变量的函数
  • JavaScript权威指南:从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链
  • 你不知道的JavaScript:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
function fn1(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log('hahaha')
  }
}
let fn1Child = fn1();
fn1Child()
1
2
3
4
5
6
7
8

显然它是一个典型闭包,但是它并没有造成内存泄漏。

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2();
fn2Child()
1
2
3
4
5
6
7
8
9

显然它也是闭包,并且因为 return 的函数中存在函数 fn2 中的 test 变量引用,所以 test 并不会被回收,也就造成了内存泄漏。

其实在函数调用后,把外部的引用关系置空就好了:

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2();
fn2Child()
fn2Child = null;
1
2
3
4
5
6
7
8
9
10

# 1.2 隐式全局变量

function test() {
  // 没有声明从而制造了隐式全局变量 test1
  test1 = 'aaa';
  // 函数内部 this 指向 window,制造了隐式全局变量 test2
  this.test2 = 'bbb';
} 
1
2
3
4
5
6

# 1.3 游离DOM引用

<div id="root">
  <ul id="ul">
    <li></li>
    <li></li>
    <li id="li3"></li>
    <li></li>
  </ul>
</div>
<script>
  let root = document.querySelector("#root");
  let ul = document.querySelector("#ul");
  let li3 = document.querySelector("#li3");

  // 由于ul变量存在,整个ul及其子元素都不能GC
  root.removeChild(ul)

  // 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
  ul = null

  // 已无变量引用,此时可以GC
  li3 = null
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

如上所示,当我们使用变量缓存 DOM 节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏。

# 1.4 遗忘的定时器

let name = 'Jake'; 
setInterval(() => { 
 console.log(name); 
}, 100);
1
2
3
4

只要定时器一直运行,回调函数中引用的 name 就会一直占用内存。

当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout来清除,另外,浏览器中的 requestAnimationFrame 也存在这个问题,我们需要在不需要的时候用 cancelAnimationFrame API 来取消使用。

# 1.5 遗忘的事件监听器

当事件监听器在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这样就造成意外的内存泄漏。

<template>
  <div></div>
</template>
<script>
  export default {
    created() {
      window.addEventListener("resize", this.doSomething);
    },
    beforeDestroy() {
      window.removeEventListener("resize", this.doSomething);
    },
    methods: {
      doSomething() {
        // do something
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 1.6 遗忘的监听者模式

当我们实现了监听者模式并在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这样也会造成意外的内存泄漏。

<template>
  <div></div>
</template>
<script>
  export default {
    created() {
      eventBus.on("test", this.doSomething)
    },
    beforeDestroy() {
      eventBus.on("test", this.doSomething)
    },
    methods: {
      doSomething() {
        // do something
      }
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

如上,我们只需在 beforeDestroy 组件销毁生命周期里将其清除即可。

# 1.7 遗忘的Map、Set对象

当使用 Map 或 Set 存储对象时,同 Object 一致都是强引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收。

如果使用 Map ,对于键为对象的情况,可以采用 WeakMap,WeakMap 对象同样用来保存键值对,对于键是弱引用(注:WeakMap 只对于键是弱引用),且必须为一个对象,而值可以是任意的对象或者原始值,由于是对于对象的弱引用,不会干扰 JS 的垃圾回收。

如果需要使用 Set 引用对象,可以采用 WeakSet,WeakSet 对象允许存储对象弱引用的唯一值,WeakSet 对象中的值同样不会重复,且只能保存对象的弱引用,同样由于是对于对象的弱引用,不会干扰 JS 的垃圾回收。

// obj是一个强引用,对象存于内存,可用
let obj = {id:1}

// 重新obj引用
obj = null
// 对象从内存移除,回收 {id: 1} 对象
1
2
3
4
5
6

使用Map和Set:

let obj = {id:1}
let user = {info: obj}
let set = new Set([obj])
let map = new Map([[obj, 'hahaha']])

// 重新obj
obj = null

console.log(user.info) // {id: 1}
console.log(set)
console.log(map)
1
2
3
4
5
6
7
8
9
10
11

此例我们重写 obj 以后,{id: 1} 依然会存在于内存中,因为 user 对象以及后面的 set/map 都强引用了它,Set/Map、对象、数组对象等都是强引用,所以我们仍然可以获取到 {id: 1} ,我们想要清除那就只能重写所有引用将其置空了。

使用WeakMap 以及 WeakSet:

let obj = {id: 1}
let weakSet = new WeakSet([obj])
let weakMap = new WeakMap([[obj, 'hahaha']])

// 重写obj引用
obj = null
// {id: 1} 将在下一次 GC 中从内存中删除
1
2
3
4
5
6
7

# 1.8 未清理的Console输出

浏览器保存了我们输出对象的信息数据引用,也正是因此未清理的 console 如果输出了对象也会造成内存泄漏。

# 2、内存三大件

其实前端关于内存方面主要有三个问题,我把它们亲切的称作内存三大件:

  • 内存泄漏 我们说很久了,对象已经不再使用但没有被回收,内存没有被释放,即内存泄漏,那想要避免就避免让无用数据还存在引用关系,也就是多注意我们上面说的常见的几种内存泄漏的情况。
  • 内存膨胀 即在短时间内内存占用极速上升到达一个峰值,想要避免需要使用技术手段减少对内存的占用。
  • 频繁 GC 同这个名字,就是 GC 执行的特别频繁,一般出现在频繁使用大的临时变量导致新生代空间被装满的速度极快,而每次新生代装满时就会触发 GC,频繁 GC 同样会导致页面卡顿,想要避免的话就不要搞太多的临时变量,因为临时变量不用了就会被回收,这和我们内存泄漏中说避免使用全局变量冲突,其实,只要把握好其中的度,不太过分就 OK
上次更新: 3/9/2022, 6:56:39 PM