JavaScript如何工做:垃圾回收機制 + 常見的4種內存泄漏

原文地址: How JavaScript works: memory management + how to handle 4 common memory leaksjavascript

本文永久連接: https://github.com/AttemptWeb/Record/blob/master/js/JsMd/GcHandle.mdjava

有部分的刪減和修改,不過大部分是參照原文來的,翻譯的目的主要是弄清JavaScript的垃圾回收機制,以爲有問題的歡迎指正。git

JavaScript 中的內存分配

如今咱們將解釋第一步(分配內存)是如何在JavaScript中工做的。github

JavaScript 減輕了開發人員處理內存分配的責任 - JavaScript本身執行了內存分配,同時聲明瞭值。web

var n = 374; // 爲number分配內存
var s = 'sessionstack'; // 爲string分配內存  
var o = {
  a: 1,
  b: null
}; //爲對象及屬性分配內存 

function f(a) {
  return a + 3;
} // 爲函數分配內存
// 函數表達式分配內存
someElement.addEventListener('click', function() {
  someElement.style.backgroundColor = 'blue';
}, false);

在 JavaScript 中使用內存

基本上在 JavaScript 中使用分配的內存,意味着在其中讀寫。算法

這能夠經過讀取或寫入變量或對象屬性的值,甚至傳遞一個變量給函數來完成。數組

垃圾回收機制

因爲發現一些內存是否「再也不須要」事實上是不可斷定的,因此垃圾收集在實施通常問題解決方案時具備侷限性。下面將解釋主要垃圾收集算法及其侷限性的基本概念。瀏覽器

內存引用

若是一個對象能夠訪問另外一個對象(能夠是隱式的或顯式的),則稱該對象引用另外一個對象。例如, 一個 JavaScript 引用了它的 prototype (隱式引用)和它的屬性值(顯式引用)。緩存

在這種狀況下,「對象」的概念擴展到比普通JavaScript對象更普遍的範圍,幷包含函數做用域(或全局詞法範圍)。網絡

詞法做用域定義了變量名如何在嵌套函數中解析:即便父函數已經返回,內部函數仍包含父函數的做用域。

引用計數垃圾收集

這是最簡單的垃圾收集算法。 若是有零個指向它的引用,則該對象被認爲是「可垃圾回收的」。
請看下面的代碼:

var o1 = {
  o2: {
    x: 1
  }
};
// 兩個對象被建立。
// ‘o1’對象引用‘o2’對象做爲其屬性。
// 不能夠被垃圾收集

var o3 = o1; // ‘o3’變量是第二個引用‘o1‘指向的對象的變量. 
                                                       
o1 = 1;      // 如今,在‘o1’中的對象只有一個引用,由‘o3’變量表示

var o4 = o3.o2; // 對象的‘o2’屬性的引用.
                // 此對象如今有兩個引用:一個做爲屬性、另外一個做爲’o4‘變量

o3 = '374'; // 原來在「o1」中的對象如今爲零,對它的引用能夠垃圾收集。
            // 可是,它的‘o2’屬性存在,由‘o4’變量引用,所以不能被釋放。

o4 = null; // ‘o1’中最初對象的‘o2’屬性對它的引用爲零。它能夠被垃圾收集。

週期產生問題

在週期循環中有一個限制。在下面的例子中,兩個對象被建立並相互引用,這就建立了一個循環。在函數調用以後,它們會超出界限,因此它們其實是無用的,而且能夠被釋放。然而,引用計數算法認爲,因爲兩個對象中的每個都被至少引用了一次,因此二者都不能被垃圾收集。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // ‘o1’ 應用 ‘02’ o1 references o2
  o2.p = o1; // ‘o2’ 引用 ‘o2’ . 一個循環被建立
}
f();


<!-- -->

標記和掃描算法

爲了肯定是否須要某個對象,本算法判斷該對象是否可訪問。

標記和掃描算法通過這 3 個步驟:

1.根節點:通常來講,根是代碼中引用的全局變量。例如,在 JavaScript 中,能夠充當根節點的全局變量是「window」對象。Node.js 中的全局對象被稱爲「global」。完整的根節點列表由垃圾收集器構建。

2.而後算法檢查全部根節點和他們的子節點而且把他們標記爲活躍的(意思是他們不是垃圾)。任何根節點不能訪問的變量將被標記爲垃圾

3.最後,垃圾收集器釋放全部未被標記爲活躍的內存塊,並將這些內存返回給操做系統。


<!-- -->

標記和掃描算法行爲的可視化。

由於「一個對象有零引用」致使該對象不可達,因此這個算法比前一個算法更好。咱們在週期中看到的情形恰巧相反,是不正確的。
截至 2012 年,全部現代瀏覽器都內置了標記掃描式的垃圾回收器。去年在 JavaScript 垃圾收集(通用/增量/併發/並行垃圾收集)領域中所作的全部改進都是基於這種算法(標記和掃描)的實現改進,但這不是對垃圾收集算法自己的改進,也不是對判斷一個對象是否可訪問這個目標的改進。

週期再也不是問題

在上面的例子中,函數調用返回後,兩個對象再也不被全局對象中的變量引用。所以,垃圾收集器會認爲它們不可訪問。


<!-- -->

即便兩個對象之間有引用,根節點它們不在被訪問。

統計垃圾收集器的直觀行爲

儘管垃圾收集器很方便,但他們也有本身的一套策略。其中之一是不肯定性。換句話說,GC(垃圾收集器)是不可預測的。你不能肯定一個垃圾收集器什麼時候會執行收集。這意味着在某些狀況下,程序其實須要更多的內存。其餘狀況下,在特別敏感的應用程序中,短暫和卡頓多是明顯的。儘管不肯定性意味着不能肯定一個垃圾收集器什麼時候執行收集,大多數 GC 共享分配中的垃圾收集通用模式。若是沒有執行分配,大多數 GC 保持空閒狀態。考慮以下場景:

1.大量的分配被執行。

2.大多數這些元素(或所有)被標記爲不可訪問(假設咱們廢除一個指向咱們再也不須要的緩存的引用)。

3.沒有執行更深的內存分配。

在這種狀況下,大多數 GC 不會運行任何更深層次的收集。換句話說,即便存在引用可用於收集,收集器也不會收集這些引用。這些並非嚴格的泄漏,但仍會致使高於平常的內存使用率。

什麼是內存泄漏?

內存泄漏是應用程序過去使用,但再也不須要的還沒有返回到操做系統或可用內存池的內存片斷。因爲沒有被釋放而致使的,它將可能引發程序的卡頓和崩潰。

JavaScript 常見的四種內存泄漏

1:全局變量

function foo(arg) {
    bar = "some text";
    // window.bar = "some text";
}

假設 bar 的目的只是引用 foo 函數中的一個變量。然而不使用 var 來聲明它,就會建立一個冗餘的全局變量。

你能夠經過在 JavaScript 文件的開頭添加 'use strict'; 來避免這些後果,這將開啓一種更嚴格的 JavaScript 解析模式,從而防止意外建立全局變量。

意外的全局變量固然是個問題,然而更常出現的狀況是,你的代碼會受到顯式的全局變量的影響,而這些全局變量沒法經過垃圾收集器收集。須要特別注意用於臨時存儲和處理大量信息的全局變量。若是你必須使用全局變量來存儲數據,當你這樣作的時候,要保證一旦完成使用就把他們賦值爲 null 或從新賦值 。

2:被忘記的定時器或者回調函數

咱們以常常在 JavaScript 中使用的 setInterval 爲例。

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每5秒執行一次.

上面的代碼片斷顯示了使用定時器引用節點或無用數據的後果。它既不會被收集,也不會被釋放。沒法被垃圾收集器收集,頻繁的被調用,佔用內存。

而正確的使用方法是,確保一旦依賴於它們的事件已經處理完成,就經過明確的調用來刪除它們。

3:閉包

閉包是JavaScript開發的一個關鍵點:一個內部函數能夠訪問外部(封閉)函數的變量。

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // originalThing 被引用
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);

一旦調用了 replaceThing 函數,theThing 就獲得一個新的對象,它由一個大數組和一個新的閉包(someMethod)組成。然而 originalThing 被一個由 unused 變量(這是從前一次調用 replaceThing 變量的 Thing 變量)所持有的閉包所引用。須要記住的是一旦爲同一個父做用域內的閉包建立做用域,做用域將被共享。

在個例子中,someMethod 建立的做用域與 unused 共享。unused 包含一個關於 originalThing 的引用。即便 unused 從未被引用過,someMethod 也能夠經過 replaceThing 做用域以外的 theThing 來使用它(例如全局的某個地方)。因爲 someMethod 與 unused 共享閉包範圍,unused 指向 originalThing 的引用強制它保持活動狀態(兩個閉包之間的整個共享範圍)。這阻止了它們的垃圾收集。

在上面的例子中,爲閉包 someMethod 建立的做用域與 unused 共享,而 unused 又引用 originalThing。someMethod 能夠經過 replaceThing 範圍以外的 theThing 來引用,儘管 unused 歷來沒有被引用過。事實上,unused 對 originalThing 的引用要求它保持活躍,由於 someMethod 與 unused 的共享封閉範圍。

全部這些均可能致使大量的內存泄漏。當上面的代碼片斷一遍又一遍地運行時,您能夠預期到內存使用率的上升。當垃圾收集器運行時,其大小不會縮小。一個閉包鏈被建立(在例子中它的根就是 theThing 變量),而且每一個閉包做用域都包含對大數組的間接引用。

4: DOM 的過分引用

有些狀況下開發人員在變量中存儲 DOM 節點。假設你想快速更新表格中幾行的內容。若是在字典或數組中存儲對每一個 DOM 行的引用,就會產生兩個對同一個 DOM 元素的引用:一個在 DOM 樹中,另外一個在字典中。若是你決定刪除這些行,你須要記住讓兩個引用都沒法訪問。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')
};
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    // image 元素是body的直接子元素。
    document.body.removeChild(document.getElementById('image'));
    // 咱們仍然能夠在全局元素對象中引用button。換句話說,button元素仍在內存中,沒法由GC收集
}

在涉及 DOM 樹內的內部節點或子節點時,還有一個額外的因素須要考慮。若是你在代碼中保留對table表格單元格(td 標記)的引用,並決定從 DOM 中刪除該table表格但保留對該特定單元格td的引用,則能夠預見到嚴重的內存泄漏。你可能會認爲垃圾收集器會釋放除了那個單元格td以外的全部東西。但狀況並不是如此。因爲單元格td是table表格的子節點,而且子節點保持對父節點的引用,因此對table表格對單元格td的這種單引用會把整個table表格保存在內存中。

咱們在 SessionStack 嘗試遵循這些最佳實踐,編寫正確處理內存分配的代碼,緣由以下:

一旦將 SessionStack 集成到你的生產環境的 Web 應用程序中,它就會開始記錄全部的事情:全部的 DOM 更改,用戶交互,JavaScript 異常,堆棧跟蹤,失敗網絡請求,調試消息等。

經過 SessionStack web 應用程序中的問題,並查看全部的用戶行爲。全部這些都必須在您的網絡應用程序沒有性能影響的狀況下進行。

因爲用戶能夠從新加載頁面或導航你的應用程序,全部的觀察者,攔截器,變量分配等都必須正確處理,這樣它們纔不會致使任何內存泄漏,也不會增長咱們正在整合的Web應用程序的內存消耗。

這裏有一個免費的計劃因此你能夠試試看.

Resources

How JavaScript works: memory management + how to handle 4 common memory leaks

MDN 內存管理

ps: 順便推一下本身的我的公衆號:Yopai,有興趣的能夠關注,每週不按期更新,分享能夠增長世界的快樂

相關文章
相關標籤/搜索