JavaScript系列以內存泄漏

在程序運行過程當中再也不用到的內存,沒有及時釋放,會出現內存泄漏(memory leak),會形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果。javascript

而內存泄漏是每一個開發人員最終必須面對的問題。 即便使用內存管理語言,好比C語言有着malloc()free() 這種低級內存管理語言也有可能出現泄露內存的狀況。html

這很麻煩,因此爲了減輕編程中的負擔,大多數語言提供了自動內存管理,這被稱爲"垃圾回收機制"(garbage collector)。java

垃圾回收機制

如今各大瀏覽器一般採用的垃圾回收有兩種方法:標記清除(mark and sweep)引用計數(reference counting)node

一、標記清除git

這是javascript中最經常使用的垃圾回收方式。程序員

工做原理:當變量進入執行環境時,將這個變量標記爲「進入環境」。當變量離開環境時,則將其標記爲「離開環境」。標記「離開環境」的就回收內存。github

工做流程:算法

  1. 垃圾回收器,在運行的時候會給存儲在內存中的全部變量都加上標記。
  2. 去掉環境中的變量以及被環境中的變量引用的變量的標記。
  3. 以後再被加上標記的變量將被視爲準備刪除的變量。
  4. 垃圾回收器完成內存清除工做,銷燬那些帶標記的值並回收他們所佔用的內存空間。

二、引用計數編程

工做原理:跟蹤記錄每一個值被引用的次數。數組

工做流程:

  1. 將一個引用類型的值賦值給這個聲明瞭的變量,這個引用類型值的引用次數就是1。
  2. 同一個值又被賦值給另外一個變量,這個引用類型值的引用次數加1。
  3. 當包含這個引用類型值的變量又被賦值成另外一個值了,那麼這個引用類型值的引用次數減1
  4. 當引用次數變成0時,就表示這個值再也不用到了。
  5. 當垃圾收集器下一次運行時,它就會釋放引用次數是0的值所佔的內存。

但若是一個值再也不須要了,引用數卻不爲0,垃圾回收機制沒法釋放這塊內存,會致使內存泄漏。

var arr = [1, 2, 3];
console.log('hello miqilin');
複製代碼

上面代碼中,數組[1, 2, 3]會佔用內存,賦值給了變量arr,所以引用次數爲1。儘管後面的一段代碼沒有用到arr,它仍是會持續佔用內存。

若是增長一行代碼,解除arr對[1, 2, 3]引用,這塊內存就能夠被垃圾回收機制釋放了。

var arr = [1, 2, 3];
console.log('hello miqilin');
arr = null;
複製代碼

上面代碼中,arr重置爲null,就解除了對[1, 2, 3]的引用,引用次數變成了0,內存就能夠釋放出來了。

所以,並非說有了垃圾回收機制,程序員就無事一身輕了。你仍是須要關注內存佔用:那些很佔空間的值,一旦再也不用到,你必須檢查是否還存在對它們的引用。若是是的話,就必須手動解除引用。

接下來,我將介紹四種常見的JavaScript 內存泄漏及如何避免。目前水平有限,借鑑了國外大牛的文章瞭解這幾種內存泄漏,原文連接:blog.sessionstack.com/how-javascr…

四種常見的 JavaScript 內存泄漏

1.意外的全局變量

未定義的變量會在全局對象建立一個新變量,對於在瀏覽器的狀況下,全局對象是window。 看如下代碼:

function foo(arg) {
     bar = "this is a hidden global variable"; 
}
複製代碼

函數foo內部使用var聲明,實際上JS會把bar掛載在全局對象上,意外建立一個全局變量。等同於:

function foo(arg) {
     window.bar = "this is an explicit global variable"; 
}
複製代碼

在上述狀況下, 泄漏一個簡單的字符串不會形成太大的傷害,但它確定會更糟。

另外一種能夠建立偶然全局變量的狀況是this

function foo() {
     this.variable = "potential accidental global"; 
}  
// Foo called on its own, this points to the global object (window)
// rather than being undefined. 
foo();
複製代碼

解決方法:

在 JavaScript 文件頭部加上 'use strict',使用嚴格模式避免意外的全局變量,此時上例中的this指向undefined。若是必須使用全局變量存儲大量數據時,確保用完之後把它設置爲 null 或者從新定義。

2.被遺忘的計時器或回調函數

在JavaScript中使用setInterval很是常見。

var someResource = getData(); 
setInterval(function() {
     var node = document.getElementById('Node');     
     if(node) {
         // Do stuff with node and someResource.
         node.innerHTML = JSON.stringify(someResource));
     } }, 1000);
複製代碼

上面的代碼代表,在節點node或者數據再也不須要時,定時器依舊指向這些數據。因此哪怕當node節點被移除後,interval 仍舊存活而且垃圾回收器沒辦法回收,它的依賴也沒辦法被回收,除非終止定時器。

var element = document.getElementById('button');  

function onClick(event) {
     element.innerHtml = 'text'; 
}  

element.addEventListener('click', onClick); // Do stuff 
element.removeEventListener('click', onClick); 
element.parentNode.removeChild(element); 

// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
複製代碼

對於上面觀察者的例子,一旦它們再也不須要(或者關聯的對象變成不可達),明確地移除它們很是重要。其中IE 6 是沒法處理循環引用的。由於老版本的 IE 是沒法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會致使內存泄漏。

可是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法(標記清除),已經能夠正確檢測和處理循環引用了。即回收節點內存時,沒必要非要調用removeEventListener了。

諸如jQuery之類的框架和庫在處理節點以前會刪除偵聽器(當使用它們的特定API時)。 這由庫內部處理,並確保不會產生任何泄漏,即便在有問題的瀏覽器(如舊版Internet Explorer)下運行也是如此。

3.閉包

JavaScript 開發的一個關鍵知識是閉包:這是一個內部函數,它能夠訪問外部(封閉)函數的變量。因爲 JavaScript 運行時的實現細節,用下邊這種方式可能會形成內存泄漏:

var theThing = null; 
var replaceThing = function () {
   var originalThing = theThing;   
   var unused = function () {
     if (originalThing)
       console.log("hi");   
};   
   theThing = {
     longStr: newArray(1000000).join('*'),
     someMethod: function () {
       console.log(someMessage);
     }
   };
 }; 
setInterval(replaceThing, 1000);
複製代碼

每次調用replaceThingtheThing獲得一個包含一個大數組和一個新閉包(someMethod)的新對象。同時,變量unused是一個引用originalThing的閉包(先前的replaceThing又調用了theThing)。someMethod能夠經過theThing使用,someMethodunused分享閉包做用域,儘管unused從未使用,它引用的originalThing迫使它保留在內存中(防止被回收)。須要記住的是一旦一個閉包做用域被同一個父做用域的閉包所建立,那麼這個做用域是共享的

全部這些均可能致使嚴重的內存泄漏。當上面的代碼片斷一次又一次地運行時,你能夠看到內存使用量的急劇增長。當垃圾收集器運行時,也不會減小。一個連接列表閉包被建立(在這種狀況下 theThing 變量是根源),每個閉包做用域對打數組進行間接引用。

解決方法:

replaceThing 的最後添加 originalThing = null 。將全部聯繫都切斷。

4.脫離 DOM 的引用

若是把DOM 存成字典(JSON 鍵值對)或者數組,此時,一樣的 DOM 元素存在兩個引用:一個在 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);
     // Much more logic
} 
 
function removeButton() {
     // The button is a direct child of body.
     document.body.removeChild(document.getElementById('button'));

    // At this point, we still have a reference to #button in the global
    // elements dictionary. In other words, the button element is still in
    // memory and cannot be collected by the GC. 
}
複製代碼

若是代碼中保存了表格某一個<td>的引用。未來決定刪除整個表格的時候,直覺認爲 GC 會回收除了已保存的<td>之外的其它節點。實際狀況並不是如此:此<td>是表格的子節點,子元素與父元素是引用關係。因爲代碼保留了<td>的引用,致使整個表格仍待在內存中。因此保存 DOM 元素引用的時候,要當心謹慎。

避免內存泄漏

在局部做用域中,等函數執行完畢,變量就沒有存在的必要了,js垃圾回收機制很快作出判斷而且回收,可是全局變量何時須要自動釋放內存空間則很難判斷,所以在咱們的開發中,須要儘可能避免使用全局變量。

咱們在使用閉包的時候,就會形成嚴重的內存泄漏,由於閉包的緣由,局部變量會一直保存在內存中,因此在使用閉包的時候,要多加當心。

Resources

若是有別的關於內存泄漏好的資源,能夠分享給我嘛謝謝了~

本人Github連接以下,歡迎各位Star

github.com/miqilin21/m…

相關文章
相關標籤/搜索