js内存泄漏及排查详解
常见内存泄漏及解决方案
内存泄漏(Memory Leak
)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
隐式全局变量
在局部作用域中,等函数执行完毕,变量就没有存在的必要了,浏览器的垃圾回收机制很快进行回收,但是对于全局变量,很难判断什么时候不用这些变量,无法正常回收;所以,尽量少使用全局变量。
function foo() {
a = 'test'
}
// 上面的写法等价于
function foo() {
window.a = 'test'
}
上面的a
变量应该是foo()
内部作用域变量的引用,由于没有使用关键词(let
、const
、var
)来声明这个变量,这时变量a
就被创建成了全局变量,这时候就会导致内存泄漏。
解决方式:使用 var
、let
、const
来定义变量。或者在js文件开头添加 'use strict'
,开启严格模式。
function bar() {
this.a = 'test'
// 函数自身发生调用,this指向全局对象window
}
bar();
闭包
闭包是代码块和创建该代码块的上下文中数据的结合(函数套函数,子函数引用了父函数的参数或变,并且被外部引用,形成不被释放的作用域)。
在使用闭包的时候,就会造成严重的内存泄漏,因为闭包中的局部变量,会一直保存在内存中。
function fn(){
let result = {}
return function(){
// 因为闭包内引用了result,导致它不会被垃圾机制回收,导致内存泄漏
return result;
}
}
let fn1 = fn()
fn1()
上面的代码可以直接通过将fn1
置为null
来清除引用。也可以少用闭包的方式。
未清除的DOM引用
<div id="app">
<ul id="ul">
<li></li>
<li></li>
<li id="li3"></li>
<li></li>
</ul>
</div>
<script>
let app = document.querySelector('#app')
let ul = document.querySelector('#ul')
let li3 = document.querySelector('#li3');
app.removeChild(ul);
</script>
上面的代码虽然调用removeChild
把ul
从DOM
上移除了,但是由于ul
变量中仍存在引用,整个ul
及子元素都不能被垃圾回收机制清除。
因此需要手动将引用清除:
ul = null;
但是此时li3
变量还引用着ul
的子节点,ul
还是不能够垃圾回收机制清除,还需要手动将li3
解除引用。
li3 = null;
定时器
setInterval
或者setTimeout
在不需要使用的时候,没有被clear
,导致定时器的回调函数及其内部依赖的变量都不能被回收,这也会造成内存泄漏。另外,浏览器中的 requestAnimationFrame
也存在这个问题,在不需要的时候用 cancelAnimationFrame
来取消使用。
const data = {};
setInterval(() => {
console.log(data);
}, 1000)
循环引用
循环引用 在引用计数策略下会导致内存泄漏,标记清除不会。
function fn() {
const a = {};
const b = {};
a.b = b;
b.a = a;
}
fn();
a
和b
的引用次数都是2,fn()
执行完毕后,两个对象都已经离开环境。
在标记清除方式下是没有问题的,但是在引用计数策略下,a
和b
的引用次数不为0,不会被垃圾回收器回收内存。如果fn
函数被大量调用,就会造成内存泄漏,这时候就需要手动解除引用(置为null
)。
未清理的console
如果在console
中输出了对象,那么浏览器就需要把这个引用关系保存下来,才能在控制台上看到相应的对象,这样同样也会造成内存泄漏。
使用chrome devtool工具排查内存泄漏问题
查看内存曲线
在浏览器中打开开发者工具(通常都是F12
快捷键打开)。首先可以查看Performance
栏。
勾选memory
,点击左上角的原点开始录制一段时间,如果出现内存曲线没有明显下降,说明可能存在内存泄漏。
查看内存情况
在performance
栏中如果看到曲线没有明显下降, 那么这时候就可以点击memory
栏去查看更多的信息。
同样开始点击左上角的原点开始记录,通常需要录制多几遍(每次录制前都先点击垃圾回收按钮先回收掉可以收集的垃圾),然后进行对比。
这里可以很明显看到内存占用一次比一次高,选择快照对比,进行内存泄漏的排查。
首先可以先查看shallow size
(对象本身占用内存的大小,不包含其引用的对象),retained size
(对象本身的Shallow Size
+ 对象能直接或间接访问到的对象的Shallow Size
),如果retained size
远大于shallow size
,说明就是这里有泄漏。
在这个记录中,示例代码是通过新增了19
个隐式全局变量且每个变量的值new Array(100000)
都是导致的内存泄漏。
而对于未清除的DOM
引用,我们可以查看快照中有没有Detached XXXXElement
对象。
建议
文章中使用的demo
都是简单的,对于项目中想排查问题来说多了非常多变量,想要定位问题比较困难,这里个人列举几个比较有用的建议:
- 尽量使用没有混淆的代码:
打包后的代码往往经过了混淆和压缩,在生产环境上这是必要的,但在debug
时却会成为我们的绊脚石,不便于阅读。
- 排查问题时使用
production
模式编译出来的代码:
dev
模式下往往会开启一些方便开发的特性,例如热更新等。但它们可能会占用一部分的内存,影响到内存问题的排查,所以建议还是使用production
模式编译出来的代码进行问题排查。
- 屏蔽所有浏览器插件:
屏蔽浏览器插件最快的方式就是打开无痕窗口。浏览器插件给我们带来很多便利,但插件注入的额外逻辑有时也会影响内存问题的排查。例如vue-devtools
会记录下每一个vuex mutaions
,导致内存无法释放。
- 在现场打内存快照,便于跳转到源代码所在行:
尽管devTools
记录下来的内存快照文件可以单独加载展示,但还是建议在记录下内存快照的时候“趁热”分析,因为这时还能从retaining tree
上跳转到代码所在行,有时候对定位问题也很有帮助。