关于js内存回收机制及内存泄漏的可能缘由

1. 概述

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

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

2. 变量的生命周期

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

3. JS垃圾回收方式

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

4 标记清除(mark and sweep)

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

垃圾收集器给内存中的全部变量都加上标记,而后去掉环境中的变量以及被环境中的变量引用的变量的标记。在此以后再被加上的标记的变量即为须要回收的变量,由于环境中的变量已经没法访问到这些变量。闭包

4 引用计数(reference counting)

常见的内存泄漏

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

全局变量引发的内存泄漏函数

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

闭包引发的内存泄漏ui

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

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

<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 中定义的变量是保存在一块儿的, 因此就漏了.

相关文章
相关标签/搜索