常见内存泄漏及解决方案
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
1.隐式全局变量
在局部作用域中,等函数执行完毕,变量就没有存在的必要了,浏览器的垃圾回收机制很快进行回收,但是对于全局变量,很难判断什么时候不用这些变量,无法正常回收;所以,尽量少使用全局变量。
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();
2.闭包
闭包是代码块和创建该代码块的上下文中数据的结合(函数套函数,子函数引用了父函数的参数或变,并且被外部引用,形成不被释放的作用域)。
在使用闭包的时候,就会造成严重的内存泄漏,因为闭包中的局部变量,会一直保存在内存中。
function fn(){
let result = {}
return function(){
// 因为闭包内引用了result,导致它不会被垃圾机制回收,导致内存泄漏
return result;
}
}
let fn1 = fn()
fn1()
上面的代码可以直接通过将fn1置为null来清除引用。也可以少用闭包的方式。
3.未清除的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;
4.定时器
setInterval或者setTimeout在不需要使用的时候,没有被clear,导致定时器的回调函数及其内部依赖的变量都不能被回收,这也会造成内存泄漏。另外,浏览器中的 requestAnimationFrame 也存在这个问题,在不需要的时候用 cancelAnimationFrame 来取消使用。
const data = {};
setInterval(() => {
console.log(data);
}, 1000)
5.循环引用
循环引用 在引用计数策略下会导致内存泄漏,标记清除不会。
function fn() {
const a = {};
const b = {};
a.b = b;
b.a = a;
}
fn();
a和b的引用次数都是2,fn()执行完毕后,两个对象都已经离开环境。
在标记清除方式下是没有问题的,但是在引用计数策略下,a和b的引用次数不为0,不会被垃圾回收器回收内存。如果fn函数被大量调用,就会造成内存泄漏,这时候就需要手动解除引用(置为null)。
6.未清理的console
如果在console
中输出了对象,那么浏览器就需要把这个引用关系保存下来,才能在控制台上看到相应的对象,这样同样也会造成内存泄漏。
使用chrome devtool工具排查内存泄漏问题
查看内存曲线
访问上面的代码页面,打开开发者工具,切换至 Performance 选项,勾选 Memory 选项。
在页面上点击运行按钮,然后在开发者工具上面点击左上角的录制按钮,10 秒后在页面上点击停止按钮,5 秒停止内存录制。得到内存走势如下:
如果出现内存曲线没有明显下降,说明可能存在内存泄漏。
查找内存泄漏的位置
上一步确认内存泄漏问题后,我们继续利用开发者工具进行问题查找。
访问上面的代码页面,打开开发者工具,切换至 Memory 选项。页面上点击运行按钮,然后点击开发者工具左上角的录制按钮,录制完成后继续点击录制,直到录制完成三个为止。然后点击页面上的停止按钮,在连续录制三次内存(不要清理之前的录制)。
从这里也可以看出,点击运行按钮之后,内存在不断的递增。点击停止按钮之后,内存就平稳了。虽然我们也可以用这种方式来判断是否存在内存泄漏,但是没有第一步的方法便捷,走势图也更加直观。
然后第二步的主要目的是为了记录 JavaScript 堆内存,我们可以看到哪个堆占用的内存更高。
从内存记录中,发现 array 对象占用最大,展开后发现,第一个object elements占用最大,选择这个 object elements 后可以在下面看到newArr变量,然后点击后面的高亮链接,就可以跳转到newArr附近。