JS内存泄漏实例解析

7,666 阅读3分钟

今天突然想到一个问题,let的块级作用域,以及闭包的变量引用功能很有意思(这脑洞咋联想到一起的,囧)。。闭包的使用会影响浏览器的GC过程。那么:

  • JS 对象什么时候会被自动回收
  • 如何使用正确使用闭包,并避免内存泄漏?

先看一个经典例子,循环异步打印问题(没耐心的直接跳最后一个实例(^▽^))

// 想异步打印1到5
for(var i=1; i<=5;i++) {
    setTimeout(function(){
        console.log("print: " + i);
    }, i*1000)
}
// 结果
print: 6
print: 6
print: 6
print: 6
print: 6

由于是异步调用打印函数,所以等调用这个函数时,循环已经结束,i变成了6,所以连着打印5个6。

第二种情况,如果用let 来声明i,let 和var 相比至少有如下特性:

  • let声明的变量拥有块级作用域
  • 形如for (let x...)的循环在每次迭代时都为x创建新的绑定(深度复制
// 1到5
for(let i=1; i<=5;i++) {
    setTimeout(function(){
        console.log("print: " + i);
    }, i*1000)
}
// 结果
print: 1
print: 2
print: 3
print: 4
print: 5

这种情况下直接通过let, 实际上给每一次回调函数的注册,创建了一个闭包,所以打印正常。

第三种情况,通过手动创建闭包也可以实现类似效果。每次循环内,返回一个函数引用当时的变量 i,这样实际上是重新分配了内存来存储i 的值,而不是单纯的引用内存地址。 尼玛内存蹭蹭往上涨,不过这么点数据完全不用担心

// 1到5
for(var i=1; i<=5;i++) {
    setTimeout((function(){
        var b = i; //install timer的时候引用 i 并且return 一个函数
        return function(){
            console.log("print: " + b);
        }
    })(), i*1000)
}

这个例子很好地说明了闭包对内部变量内存地址的保留作用(循环1w次就深度复制了1w份i )。但闭包和全局变量的不当使用可能会导致内存泄漏,内存居高不下甚至标签页直接挂掉。

JS 变量在浏览器内存中是否被GC 回收要看这个变量所在作用域的生命周期和变量是否被别人引用:

  • 如果是函数内部声明的变量,并且没有任何外部变量引用,则函数执行完就销毁。如果有引用,则该内部变量会一直游离于内存中

JS 对象(引用类型)是存储在内存堆heap中,可以通过Chrome Debug Tool的 Profile 工具生成Heap SnapShot 来查看。

最后看一个活生生的实例,不出意外分分钟内存占用1G

function Test()  
{  
    this.obj= {};
    this.index = 1;
    this.timer = null;
    var cache = []; // 内部变量,内存隐患...
    this.timer = window.setInterval(() =>{
        this.index += 1; 
        this.obj = {
            val: '_timerxxxxxbbbbxx_' + this.index,
            junk: [...cache]
        };
        cache.push(this.obj);
    }, 1);  
    console.warn("create Test instance..");
}  
test = new Test(); // JS对象开启定时器不断分配内存

啰嗦几句,这个例子的关键在于内部变量cache被外部的异步函数(定时器)引用。 如果不清除定时器,只是把Test类的实例手动设为null,也无济于事,cache还会继续占用内存。

在清除定时器,并且把Test类的实例设为null后才成功回收垃圾

Test.prototype.destroy = function(){
    clearInterval(this.timer);
}
function d() {
    // 取消定时器并销毁Test 实例
    test.destroy();
    test = null;
    console.warn("destroyed test instance..");
}

清除内部变量cache的引用后,内存堆大小立刻下降了40MB.

总结:

  • 函数内部不用的局部变量及时清理,在清理时要考虑ta的所有引用函数。
  • 非得引用局部变量,请用非匿名函数,否则难以销毁引用。

个人见解,说得不对之处欢迎评论指正 : )

参考文章:

es6-in-depth-let-and-const

谈一谈Javascript内存释放那点事

JS内存管理