关于js内存回收机制及内存泄漏的可能原因

494 阅读5分钟

1. 概述

JS的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。

C#、Java、JavaScript有自动垃圾回收机制,但c++和c就没有垃圾回收机制,也许是因为垃圾回收机制必须由一种平台来实现。在JS中,JS的执行环境会负责管理代码执行过程中使用的内存。

2. 变量的生命周期

当一个变量的生命周期结束之后它所指向的内存就应该被释放。JS有两种变量,全局变量和在函数中产生的局部变量。局部变量的生命周期在函数执行过后就结束了,此时便可将它引用的内存释放(即垃圾回收),但全局变量生命周期会持续到浏览器关闭页面。

3. JS垃圾回收方式

JS执行环境中的垃圾回收器怎样才能检测哪块内存可以被回收有两种方式:标记清除(mark and sweep)、引用计数(reference counting)。、

4 标记清除(mark and sweep)

大部分浏览器以此方式进行垃圾回收,当变量进入执行环境(函数中声明变量)的时候,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”,在离开环境之后还有的变量则是需要被删除的变量。标记方式不定,可以是某个特殊位的反转或维护一个列表等。

垃圾收集器给内存中的所有变量都加上标记,然后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上的标记的变量即为需要回收的变量,因为环境中的变量已经无法访问到这些变量。

4 引用计数(reference counting)

常见的内存泄漏

虽然JavaScript 会自动垃圾收集,但是如果我们的代码写法不当,会让变量一直处于“进入环境”的状态,无法被回收。下面列一下内存泄露常见的几种情况。

全局变量引起的内存泄漏

function leaks(){  
    leak = 'xxxxxx';//leak 成为一个全局变量,不会被回收
}
123

闭包引起的内存泄漏

var leaks = (function(){  
    var leak = 'xxxxxx';// 被闭包所引用,不会被回收
    return function(){
        console.log(leak);
    }
})()
123456

dom清空或删除时,事件未清除导致的内存泄漏

<div id="container">  
</div>

$('#container').bind('click', function(){
    console.log('click');
}).remove();

// zepto 和原生 js下,#container dom 元素,还在内存里jquery 的 empty和 remove会帮助开发者避免这个问题
12345678
<div id="container">  
</div>

$('#container').bind('click', function(){
    console.log('click');
}).off('click').remove();
//把事件清除了,即可从内存中移除


没有清理的DOM元素引用
var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
}

function removeButton() {
    document.body.removeChild(document.getElementById('button'));

    // 虽然我们用removeChild移除了button, 但是还在elements对象里保存着#button的引用
    // 换言之, DOM元素还在内存里面.
}

被遗忘的定时器或者回调

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

这样的代码很常见, 如果id为Node的元素从DOM中移除, 该定时器仍会存在, 同时, 因为回调函数中包含对someResource的引用, 定时器外面的someResource也不会被释放.

一个好玩的例子

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var someMessage = '123'
  theThing = {
    longStr: new Array(1000000).join('*'),        // 大概占用1MB内存
    someMethod: function () {
      console.log(someMessage);
    }
  };
};

我们先做一个假设, 如果函数中所有的私有变量, 不管someMethod用不用, 都被放进闭包的话, 那么会发生什么呢.

第一次调用replaceThing, 闭包中包含originalThing = null和someMessage = ‘123’, 我们设函数结束时, theThing的值为theThing_1.

第二次调用replaceThing, 如果我们的假设成立, originalThing = theThing_1和someMessage = ‘123’.我们设第二次调用函数结束时, theThing的值为theThing_2.注意, 此时的originalThing保存着theThing_1, theThing_1包含着和theThing_2截然不同的someMethod, theThing_1的someMethod中包含一个someMessage, 同样如果我们的假设成立, 第一次的originalThing = null应该也在.

所以, 如果我们的假设成立, 第二次调用以后, 内存中有theThing_1和theThing_2, 因为他们都是靠longStr把占用内存撑起来, 所以第二次调用以后, 内存消耗比第一次多1MB.

如果你亲自试了(使用Chrome的Profiles查看每次调用后的内存快照), 会发现我们的假设是不成立的, 浏览器很聪明, 它只会把someMethod用到的变量保存下来, 用不到的就不保存了, 这为我们节省了内存.

但如果我们这么写:

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  var someMessage = '123'
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};

unused 这个函数我们没有用到, 但是它用了 originalThing 变量, 接下来, 如果你一次次调用 replaceThing , 你会看到内存1MB 1MB的涨.

也就是说, 虽然我们没有使用 unused , 但是因为它使用了 originalThing , 使得它也被放进闭包了, 内存漏了.

强烈建议读者亲自试试在这几种情况下产生的内存变化.

这种情况产生的原因, 通俗讲, 是因为无论 someMethod 还是 unused , 他们其中所需要用到的在 replaceThing 中定义的变量是保存在一起的, 所以就漏了.

总结

image.png

image.png