關於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 中定義的變量是保存在一塊兒的, 因此就漏了.

相關文章
相關標籤/搜索