原文地址: How JavaScript works: memory management...
javascript
本文永久連接: https://github.com/AttemptWeb...
java
有部分的刪減和修改,不過大部分是參照原文來的,翻譯的目的主要是弄清JavaScript的垃圾回收機制,以爲有問題的歡迎指正。
git
如今咱們將解釋第一步(分配內存)是如何在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 中使用分配的內存,意味着在其中讀寫。算法
這能夠經過讀取或寫入變量或對象屬性的值,甚至傳遞一個變量給函數來完成。數組
因爲發現一些內存是否「再也不須要」事實上是不可斷定的,因此垃圾收集在實施通常問題解決方案時具備侷限性。下面將解釋主要垃圾收集算法及其侷限性的基本概念。
瀏覽器
prototype
(隱式引用)和它的屬性值(顯式引用)。
在這種狀況下,「對象」的概念擴展到比普通JavaScript對象更普遍的範圍,幷包含函數做用域(或全局詞法範圍)。緩存
詞法做用域定義了變量名如何在嵌套函數中解析:即便父函數已經返回,內部函數仍包含父函數的做用域。
這是最簡單的垃圾收集算法。 若是有零個指向它的引用,則該對象被認爲是可垃圾回收的
。 請看下面的代碼:bash
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個步驟
:
根節點
:通常來講,根是代碼中引用的全局變量。例如,在 JavaScript 中,能夠充當根節點的全局變量是「window」對象。Node.js 中的全局對象被稱爲「global」。完整的根節點列表由垃圾收集器構建。任何根節點不能訪問的變量將被標記爲垃圾
。垃圾收集器釋放全部未被標記爲活躍的內存塊,並將這些內存返回給操做系統
。標記和掃描算法行爲的可視化。(Mark and sweep) 標記與清除
由於「一個對象有零引用」致使該對象不可訪問,因此這個算法比前一個算法更好。咱們在週期中看到的情形恰巧相反,是不正確的。 截至 2012 年,全部現代瀏覽器都內置了標記掃描式的垃圾回收器
。去年在 JavaScript 垃圾收集(通用/增量/併發/並行垃圾收集)領域中所作的全部改進都是基於這種算法(標記和掃描)的實現改進,但這不是對垃圾收集算法自己的改進,也不是對判斷一個對象是否可訪問這個目標的改進。
在上面的例子中,函數調用返回後,兩個對象再也不被全局對象中的變量引用。所以,垃圾收集器會認爲它們不可訪問。
即便兩個對象之間有引用,根節點不在訪問它們。
儘管垃圾收集器很方便,但他們也有本身的一套策略。其中之一是不肯定性。換句話說,GC(垃圾收集器)是不可預測的。你不能肯定一個垃圾收集器什麼時候會執行收集。這意味着在某些狀況下,程序其實須要更多的內存。其餘狀況下,在特別敏感的應用程序中,短暫和卡頓多是明顯的。儘管不肯定性意味着不能肯定一個垃圾收集器什麼時候執行收集,大多數 GC 共享分配中的垃圾收集通用模式。若是沒有執行分配,大多數 GC 保持空閒狀態。考慮以下場景:
大量的分配被執行。
大多數這些元素(或所有)被標記爲不可訪問(假設咱們廢除一個指向咱們再也不須要的緩存的引用)。
沒有執行更深的內存分配。
在這種狀況下,大多數 GC 不會運行任何更深層次的收集。換句話說,即便存在變量可用於收集,收集器也不會收集這些引用。這些並非嚴格的泄漏,但仍會致使高於平常的內存使用率。
內存泄漏 是
應用程序過去使用,但再也不須要的還沒有返回到操做系統或可用內存池的內存片斷
。因爲沒有被釋放而致使的,它將可能引發程序的卡頓和崩潰。
function foo(arg) {
bar = "some text";
// window.bar = "some text";
}複製代碼
假設 bar 的目的只是引用 foo 函數中的一個變量。然而不使用 var
來聲明它,就會建立一個冗餘的全局變量
。
你能夠經過在 JavaScript 文件的開頭添加 'use strict'; 來避免這些後果,這將開啓一種更嚴格的 JavaScript 解析模式,從而防止意外建立全局變量。
意外的全局變量固然是個問題,然而更常出現的狀況是,你的代碼會受到顯式的全局變量的影響,而這些全局變量沒法經過垃圾收集器收集。須要特別注意用於臨時存儲和處理大量信息的全局變量。若是你必須使用全局變量來存儲數據,當你這樣作的時候,要保證一旦完成使用,就把他們賦值爲 null 或從新賦值
。
咱們以常常在 JavaScript 中使用的 setInterval
爲例。
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每5秒執行一次.複製代碼
上面的代碼片斷顯示了使用定時器引用節點或無用數據的後果。它既不會被收集,也不會被釋放。沒法被垃圾收集器收集,頻繁的被調用,佔用內存。而正確的使用方法是,確保一旦依賴於它們的事件已經處理完成,就經過明確的調用來刪除它們。
閉包是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
變量),而且每一個閉包做用域都包含對大數組的間接引用。
有些狀況下開發人員在變量中存儲 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應用程序的內存消耗。
這裏有一個免費的計劃因此你能夠試試看.
How JavaScript works: memory management...