js内存泄露相关知识
邵预鸿 Lv5

「硬核JS」你的程序中可能存在内存泄漏

引擎中有垃圾回收机制,它主要针对一些程序中不再使用的对象,对其清理回收释放掉内存。

那么垃圾回收机制会把不再使用的对象(垃圾)全都回收掉吗?

其实引擎虽然针对垃圾回收做了各种优化从而尽可能的确保垃圾得以回收,但并不是说我们就可以完全不用关心这块了,我们代码中依然要主动避免一些不利于引擎做垃圾回收操作,因为不是所有无用对象内存都可以被回收的,那当不再用到的对象内存,没有及时被回收时,我们叫它 内存泄漏(Memory leak)

常见的内存泄漏

代码不规范,同事两行泪,接下来我们看看会引起内存泄漏的一些常见案例。

不正当的闭包

闭包就是函数内部嵌套并 return 一个函数???这是大多数人认为的闭包,好吧,它确实也是,我们来看看几本 JS 高光书中的描述:

  • JavaScript高级程序设计:闭包是指有权访问另一个函数作用域中的变量的函数
  • JavaScript权威指南:从技术的角度讲,所有的JavaScript函数都是闭包:它们都是对象,它们都关联到作用域链
  • 你不知道的JavaScript:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行

按照上面三本书中的描述,那闭包所涉及的的范围就比较广了,我们这里暂时不去纠结闭包的定义,就以最简单、大家都认可的闭包例子来看闭包:

1
2
3
4
5
6
7
8
9
function fn1(){
let test = 'isboyjc'
return function(){
console.log('hahaha')
}
}
let fn1Child = fn1()
fn1Child()
复制代码

上例是闭包吗?它造成内存泄漏了吗?

显然它是一个典型闭包,但是它并没有造成内存泄漏,因为返回的函数中并没有对 fn1 函数内部的引用,也就是说,函数 fn1 内部的 test 变量完全是可以被回收的,那我们再来看:

1
2
3
4
5
6
7
8
9
10
function fn2(){
let test = 'isboyjc'
return function(){
console.log(test)
return test
}
}
let fn2Child = fn2
fn2Child()
复制代码

上例是闭包吗?它造成内存泄漏了吗?

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

那么怎样解决呢?

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

1
2
3
4
5
6
7
8
9
10
11
function fn2(){
let test = 'isboyjc'
return function(){
console.log(test)
return test
}
}
let fn2Child = fn2
fn2Child()
fn2Child = null
复制代码

“ 减少使用闭包,闭包会造成内存泄漏。。。 ”

醒醒,这句话是过去式了,它的描述不准确,So,应该说不正当的使用闭包可能会造成内存泄漏。

隐式全局变量

我们知道 JavaScript 的垃圾回收是自动执行的,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。

再来看全局变量和局部变量,函数中的局部变量在函数执行结束后这些变量已经不再被需要,所以垃圾回收器会识别并释放它们。但是对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以全局变量通常不会被回收,我们使用全局变量是 OK 的,但同时我们要避免一些额外的全局变量产生,如下:

1
2
3
4
5
6
7
8
9
function fn(){
// 没有声明从而制造了隐式全局变量test1
test1 = 'isboyjc1'

// 函数内部this指向window,制造了隐式全局变量test2
this.test2 = 'isboyjc2'
}
fn()
复制代码

调用函数 fn ,因为 没有声明 和 函数中this 的问题造成了两个额外的隐式全局变量,这两个变量不会被回收,这种情况我们要尽可能的避免,在开发中我们可以使用严格模式或者通过 lint 检查来避免这些情况的发生,从而降低内存成本。

除此之外,我们在程序中也会不可避免的使用全局变量,这些全局变量除非被取消或者重新分配之外也是无法回收的,这也就需要我们额外的关注,也就是说当我们在使用全局变量存储数据时,要确保使用后将其置空或者重新分配,当然也很简单,在使用完将其置为 null 即可,特别是在使用全局变量做持续存储大量数据的缓存时,我们一定要记得设置存储上限并及时清理,不然的话数据量越来越大,内存压力也会随之增高。

1
2
3
4
5
6
var test = new Array(10000)

// do something

test = null
复制代码

游离DOM引用

考虑到性能或代码简洁方面,我们代码中进行 DOM 时会使用变量缓存 DOM 节点的引用,但移除节点的时候,我们应该同步释放缓存的引用,否则游离的子树无法释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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>

复制代码

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

假如我们将父节点置空,但是被删除的父节点其子节点引用也缓存在变量里,那么就会导致整个父 DOM 节点树下整个游离节点树均无法清理,还是会出现内存泄漏,解决办法就是将引用子节点的变量也置空,如下图:

img

遗忘的定时器

程序中我们经常会用到计时器,也就是 setTimeoutsetInterval,先来看一个例子:

1
2
3
4
5
6
7
8
9
// 获取数据
let someResource = getData()
setInterval(() => {
const node = document.getElementById('Node')
if(node) {
node.innerHTML = JSON.stringify(someResource))
}
}, 1000)
复制代码

上面是我随便 copy 的一个小例子,其代码中每隔一秒就将得到的数据放入到 Node 节点中去,但是在 setInterval 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。

什么才叫结束呢?也就是调用了 clearInterval。如果没有被 clear 掉的话,就会造成内存泄漏。不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收。所以在上例中,someResource 就没法被回收。

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

遗忘的事件监听器

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

我们就拿 Vue 组件来举例子,React 里也是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div></div>
</template>

<script>
export default {
created() {
window.addEventListener("resize", this.doSomething)
},
beforeDestroy(){
window.removeEventListener("resize", this.doSomething)
},
methods: {
doSomething() {
// do something
}
}
}
</script>
复制代码

遗忘的监听者模式

监听者模式想必我们都知道,不管是 Vue 、 React 亦或是其他,对于目前的前端开发框架来说,监听者模式实现一些消息通信都是非常常见的,比如 EventBus. . .

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

还是用 Vue 组件举例子,因为比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div></div>
</template>

<script>
export default {
created() {
eventBus.on("test", this.doSomething)
},
beforeDestroy(){
eventBus.off("test", this.doSomething)
},
methods: {
doSomething() {
// do something
}
}
}
</script>
复制代码

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

遗忘的Map、Set对象

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

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

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

这里可能需要简单介绍下,谈弱引用,我们先来说强引用,之前我们说 JS 的垃圾回收机制是如果我们持有对一个对象的引用,那么这个对象就不会被垃圾回收,这里的引用,指的就是 强引用 ,而弱引用就是一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,因此可能在任何时刻被回收

** 不明白?来看例子就晓得了:

1
2
3
4
5
6
7
// obj是一个强引用,对象存于内存,可用
let obj = {id: 1}

// 重写obj引用
obj = null
// 对象从内存移除,回收 {id: 1} 对象
复制代码

上面是一个简单的通过重写引用来清除对象引用,使其可回收。

再看下面这个:

1
2
3
4
5
6
7
8
9
10
11
12
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)
复制代码

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

接下来我们看 WeakMap 以及 WeakSet

1
2
3
4
5
6
7
8
9
let obj = {id: 1}
let weakSet = new WeakSet([obj])
let weakMap = new WeakMap([[obj, 'hahaha']])

// 重写obj引用
obj = null

// {id: 1} 将在下一次 GC 中从内存中删除
复制代码

如上所示,使用了 WeakMap 以及 WeakSet 即为弱引用,将 obj 引用置为 null 后,对象 {id: 1} 将在下一次 GC 中被清理出内存。

未清理的Console输出

写代码的过程中,肯定避免不了一些输出,在一些小团队中可能项目上线也不清理这些 console,殊不知这些 console 也是隐患,同时也是容易被忽略的,我们之所以在控制台能看到数据输出,是因为浏览器保存了我们输出对象的信息数据引用,也正是因此它也会造成内存泄漏。

所以,开发环境下我们可以使用控制台输出来便于我们调试,但是在生产环境下,一定要及时清理掉输出。

作者:isboyjc链接:https://juejin.cn/post/6984188410659340324来源:掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 本文标题:js内存泄露相关知识
  • 本文作者:邵预鸿
  • 创建时间:2021-07-13 09:51:55
  • 本文链接:/images/logo.jpg2021/07/13/js内存泄露相关知识/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!