如何處理JavaScript內(nèi)存泄露
幾周前,我們開(kāi)始寫一個(gè)系列,深入探討JavaScript和它的工作原理。我們認(rèn)為了解JavaScript的構(gòu)成以及它們?nèi)绾螀f(xié)作,有助于編寫出更好的代碼和應(yīng)用程序。
本系列***篇重點(diǎn)介紹了引擎、運(yùn)行時(shí)、調(diào)用棧。第二篇揭示了谷歌V8 JavaScript引擎的內(nèi)部機(jī)制,并且提供了一些關(guān)于如何寫出更好的JavaScript代碼的建議。
本文作為第三篇,將會(huì)討論另一個(gè)開(kāi)發(fā)者容易忽視的重要主題 :內(nèi)存管理。我們也會(huì)提供一些關(guān)于如何處理JavaScript內(nèi)存泄露的技巧。在SessionStack,我們需要確保不會(huì)造成內(nèi)存泄露或者不會(huì)增加我們集成的Web應(yīng)用的內(nèi)存消耗。
概述
某些語(yǔ)言,比如C有低級(jí)的原生內(nèi)存管理原語(yǔ),像malloc()和free()。開(kāi)發(fā)人員使用這些原語(yǔ)可以顯式分配和釋放操作系統(tǒng)的內(nèi)存。
相對(duì)地,JavaScript會(huì)在創(chuàng)建變量(對(duì)象、字符串)時(shí)自動(dòng)分配內(nèi)存,并在這些變量不被使用時(shí)自動(dòng)釋放內(nèi)存,這個(gè)過(guò)程被稱為垃圾回收。這個(gè)“自動(dòng)”釋放資源的特性帶來(lái)了很多困惑,讓JavaScript(和其他高級(jí)級(jí)語(yǔ)言)開(kāi)發(fā)者誤以為可以不關(guān)心內(nèi)存管理。這是一個(gè)很大的錯(cuò)誤
即使使用高級(jí)級(jí)語(yǔ)言,開(kāi)發(fā)者也應(yīng)該對(duì)于內(nèi)存管理有一定的理解(至少有基本的理解)。有時(shí)自動(dòng)內(nèi)存管理存在一些問(wèn)題(例如垃圾回收實(shí)現(xiàn)可能存在缺陷或者不足),開(kāi)發(fā)者必須弄明白這些問(wèn)題,以便找一個(gè)合適解決方法。
內(nèi)存生命周期
無(wú)論你用哪一種編程語(yǔ)言,內(nèi)存生命周期幾乎總是一樣的:
Here is an overview of what happens at each step of the cycle: 這是對(duì)生命周期中的每一步大概的說(shuō)明:
- 分配內(nèi)存— 內(nèi)存是被操作系統(tǒng)分配,這允許程序使用它。在低級(jí)語(yǔ)言中(例如C),這是一個(gè)作為開(kāi)發(fā)者需要處理的顯式操作。在高級(jí)語(yǔ)言中,然而,這些操作都代替開(kāi)發(fā)者進(jìn)行了處理。
 - 使用內(nèi)存。實(shí)際使用之前分配的內(nèi)存,通過(guò)在代碼操作變量對(duì)內(nèi)在進(jìn)行讀和寫。
 - 釋放內(nèi)存 。不用的時(shí)候,就可以釋放內(nèi)存,以便重新分配。與分配內(nèi)存操作一樣,釋放內(nèi)存在低級(jí)語(yǔ)言中也需要顯式操作。
 
想要快速的了解堆棧和內(nèi)存的概念,可以閱讀本系列***篇文章。
什么是內(nèi)存
在直接探討Javascript中的內(nèi)存之前,我們先簡(jiǎn)要的討論一下什么是內(nèi)存、內(nèi)存大概是怎么樣工作的。
在硬件中,電腦的內(nèi)存包含了大量的觸發(fā)電路,每一個(gè)觸發(fā)電路都包含一些能夠儲(chǔ)存1位數(shù)據(jù)的晶體管。觸發(fā)器通過(guò)唯一標(biāo)識(shí)符來(lái)尋址,從而可以讀取和覆蓋它們。因此,從概念上來(lái)講,可以認(rèn)為電腦內(nèi)存是一個(gè)巨大的可讀寫陣列。
人類不善于把我們所有的思想和算術(shù)用位運(yùn)算來(lái)表示,我們把這些小東西組織成一個(gè)大家伙,這些大家伙可以用來(lái)表現(xiàn)數(shù)字:8位是一個(gè)字節(jié)。字節(jié)之上是字(16位、32位)。
許多東西被存儲(chǔ)在內(nèi)存中:
- 所有的變量和程序中用到的數(shù)據(jù);
 - 程序的代碼,包括操作系統(tǒng)的代碼。
 
編譯器和操作系統(tǒng)共同工作幫助開(kāi)發(fā)者完成大部分的內(nèi)存管理,但是我們推薦你了解一下底層到底發(fā)生了什么。
編譯代碼的時(shí)候,編譯器會(huì)解析原始數(shù)據(jù)類型,提前計(jì)算出它們需要多大的內(nèi)存空間。然后將所需的數(shù)量分配在??臻g中。之所以稱為棧空間,是因在函數(shù)被調(diào)用的時(shí)候,他們的內(nèi)存被添加在現(xiàn)有內(nèi)存之上(就是會(huì)在棧的最上面添加一個(gè)棧幀來(lái)指向存儲(chǔ)函數(shù)內(nèi)部變量的空間)。終止的時(shí)候,以LIFO(后進(jìn)先出)的順序移除這些調(diào)用。例如:
- int n; // 4字節(jié)
 - int x[4]; // 4個(gè)元素的數(shù)組,每個(gè)元素4字節(jié)
 - double m; // 8字節(jié)
 
編譯器馬上知道需要內(nèi)存 4 + 4 × 4 + 8 = 28字節(jié)。
這是當(dāng)前整型和雙精度的大小。大約20年以前,整型通常只需要2個(gè)字節(jié),雙精度需要4個(gè)字節(jié),你的代碼不受基礎(chǔ)數(shù)據(jù)類型大小的限制。
編譯器會(huì)插入與操作系統(tǒng)交互的代碼,來(lái)請(qǐng)求棧中必要大小的字節(jié)來(lái)儲(chǔ)存變量。
在上面的例子中,編輯器知道每個(gè)變量準(zhǔn)確的地址。事實(shí)上,無(wú)論什么時(shí)候我們寫變量n,將會(huì)在內(nèi)部被翻譯成類似“memory address 4127963”的語(yǔ)句。
注意,如果我們嘗試訪問(wèn)x[4]的內(nèi)存(開(kāi)始聲明的x[4]是長(zhǎng)度為4的數(shù)組,x[4]表示第五個(gè)元素),我們會(huì)訪問(wèn)m的數(shù)據(jù)。那是因?yàn)槲覀冋谠L問(wèn)一個(gè)數(shù)組里不存在的元素,m比數(shù)組中實(shí)際分配內(nèi)存的***一個(gè)元素x[3]要遠(yuǎn)4個(gè)字節(jié),可能***的結(jié)果是讀取(或者覆蓋)了m的一些位。這肯定會(huì)對(duì)其他程序產(chǎn)生不希望產(chǎn)生的結(jié)果。
當(dāng)函數(shù)調(diào)用其他函數(shù)的時(shí)候,每一個(gè)函數(shù)被調(diào)用的時(shí)候都會(huì)獲得自己的棧塊。在自己的棧塊里會(huì)保存函數(shù)內(nèi)所有的變量,還有一個(gè)程序計(jì)數(shù)器會(huì)記錄變量執(zhí)行時(shí)所在的位置。當(dāng)函數(shù)執(zhí)行完之后,會(huì)釋放它的內(nèi)存以作他用。
動(dòng)態(tài)分配
不幸的是,事情并不是那么簡(jiǎn)單,因?yàn)樵诰幾g的時(shí)候我們并不知道一個(gè)變量將會(huì)需要多少內(nèi)存。假設(shè)我們做了下面這樣的事:
- int n = readInput(); //讀取用戶的輸入
 - ...
 - //創(chuàng)建一個(gè)有n個(gè)元素的數(shù)組
 
編譯器不知道這個(gè)數(shù)組需要多少內(nèi)存,因?yàn)閿?shù)組大小取決于用戶提供的值。
因此,此時(shí)不能在棧上分配空間。程序必須在運(yùn)行時(shí)向操作系統(tǒng)請(qǐng)求夠用的空間。此時(shí)內(nèi)存從堆空間中被分配。靜態(tài)與動(dòng)態(tài)分配內(nèi)存之間的不同在下面的表格中被總結(jié)出來(lái):
靜態(tài)分配內(nèi)存與動(dòng)態(tài)分配內(nèi)存的區(qū)別。
為了完全理解動(dòng)態(tài)內(nèi)存是如何分配的,我們需要花更多的時(shí)間在指針上,這個(gè)可能很大程度上偏離了這篇文章的主題。如果你有興趣學(xué)習(xí)更多的知識(shí),那就在評(píng)論中讓我知道,我就可以在之后的文章中寫更多關(guān)于指針的細(xì)節(jié)。
JavaScript中的內(nèi)存分配
現(xiàn)在我們來(lái)解釋JavaScript中的***步(分配內(nèi)存)是如何工作的。
JavaScript在開(kāi)發(fā)者聲明值的時(shí)候自動(dòng)分配內(nèi)存。
- var n = 374; // 為數(shù)值分配內(nèi)存
 - var s = 'sessionstack'; //為字符串分配內(nèi)存
 - var o = {
 - a: 1,
 - b: null
 - }; //為對(duì)象和它包含的值分配內(nèi)存
 - var a = [1, null, 'str']; //為數(shù)組和它包含的值分配內(nèi)存
 - function f(a) {
 - return a + 3;
 - } //為函數(shù)(可調(diào)用的對(duì)象)分配內(nèi)存
 - //函數(shù)表達(dá)式也會(huì)分配一個(gè)對(duì)象
 - someElement.addEventListener('click', function() {
 - someElement.style.backgroundColor = 'blue';
 - }, false);
 - //一些函數(shù)調(diào)用也會(huì)導(dǎo)致對(duì)象分配
 - `var d = new Date(); // allocates a Date object` //分配一個(gè)Date對(duì)象的內(nèi)存
 - `var e = document.createElement('div'); //分配一個(gè)DOM元素的內(nèi)存
 - //方法可以分配新的值或者對(duì)象
 - var s1 = 'sessionstack';
 - var s2 = s1.substr(0, 3); //s2是一個(gè)新的字符串
 - // 因?yàn)樽址遣豢勺兊?nbsp;
 - // JavaScript可能決定不分配內(nèi)存
 - // 而僅僅存儲(chǔ) 0-3的范圍
 - var a1 = ['str1', 'str2'];
 - var a2 = ['str3', 'str4'];
 - var a3 = a1.concat(a2);
 - //新的數(shù)組有4個(gè)元素是a1和a2連接起來(lái)的。
 
在JavaScript中使用內(nèi)存
在JavaScript中使用被分配的內(nèi)存,本質(zhì)上就是對(duì)內(nèi)在的讀和寫。
比如,讀、寫變量的值或者對(duì)象的屬性,抑或向一個(gè)函數(shù)傳遞參數(shù)。
內(nèi)存不在被需要時(shí)釋放內(nèi)存
大部分的內(nèi)存管理問(wèn)題都在這個(gè)階段出現(xiàn)。
這里最難的任務(wù)是找出這些被分配的內(nèi)存什么時(shí)候不再被需要。這常常要求開(kāi)發(fā)者去決定程序中的一段內(nèi)存不在被需要而且釋放它。
高級(jí)語(yǔ)言嵌入了一個(gè)叫垃圾回收的軟件,它的工作是跟蹤內(nèi)存的分配和使用,以便于發(fā)現(xiàn)一些內(nèi)存在一些情況下不再被需要,它將會(huì)自動(dòng)地釋放這些內(nèi)存。
不幸的是,這個(gè)過(guò)程是一個(gè)近似的過(guò)程,因?yàn)橐话汴P(guān)于知道內(nèi)存是否是被需要的問(wèn)題是不可判斷的(不能用一個(gè)算法解決)。
大部分的垃圾回收器會(huì)收集不再被訪問(wèn)的內(nèi)存,例如指向它的所有變量都在作用域之外。然而,這是一組可以收集的內(nèi)存空間的近似值。因?yàn)樵谌魏螘r(shí)候,一個(gè)內(nèi)存地址可能還有一個(gè)在作用域里的變量指向它,但是它將不會(huì)被再次訪問(wèn)。
垃圾收集
由于找到一些內(nèi)存是否是“不再被需要的”這個(gè)事實(shí)是不可判定的,垃圾回收的實(shí)現(xiàn)存在局限性。本節(jié)解釋必要的概念去理解主要的垃圾回收算法和它們的局限性。
內(nèi)存引用
垃圾回收算法依賴的主要概念是引用。
在內(nèi)存管理的語(yǔ)境下,一個(gè)對(duì)象只要顯式或隱式訪問(wèn)另一個(gè)對(duì)象,就可以說(shuō)它引用了另一個(gè)對(duì)象。例如,JavaScript對(duì)象引用其Prototype(隱式引用),或者引用prototype對(duì)象的屬性值(顯式引用)。
在這種情況下,“對(duì)象”的概念擴(kuò)展到比普通JavaScript對(duì)象更廣的范圍,并且還包含函數(shù)作用域。(或者global詞法作用域)
詞法作用域定義變量的名字在嵌套的函數(shù)中如何被解析:內(nèi)部的函數(shù)包含了父級(jí)函數(shù)的作用域,即使父級(jí)函數(shù)已經(jīng)返回。
引用計(jì)數(shù)垃圾回收
這是最簡(jiǎn)單的垃圾回收算法。 一個(gè)對(duì)象在沒(méi)有其他的引用指向它的時(shí)候就被認(rèn)為“可被回收的”。
看一下下面的代碼:
- var o1 = {
 - o2: {
 - x: 1
 - }
 - };
 - //2個(gè)對(duì)象被創(chuàng)建
 - /'o2'被'o1'作為屬性引用
 - //誰(shuí)也不能被回收
 - var o3 = o1; //'o3'是第二個(gè)引用'o1'指向?qū)ο蟮淖兞?nbsp;
 - o1 = 1; //現(xiàn)在,'o1'只有一個(gè)引用了,就是'o3'
 - var o4 = o3.o2; // 引用'o3'對(duì)象的'o2'屬性
 - //'o2'對(duì)象這時(shí)有2個(gè)引用: 一個(gè)是作為對(duì)象的屬性
 - //另一個(gè)是'o4'
 - o3 = '374'; //'o1'原來(lái)的對(duì)象現(xiàn)在有0個(gè)對(duì)它的引用
 - //'o1'可以被垃圾回收了。
 - //然而它的'o2'屬性依然被'o4'變量引用,所以'o2'不能被釋放。
 - o4 = null; //最初'o1'中的'o2'屬性沒(méi)有被其他的引用了
 - //'o2'可以被垃圾回收了
 
循環(huán)引用創(chuàng)造麻煩
在涉及循環(huán)引用的時(shí)候有一個(gè)限制。在下面的例子中,兩個(gè)對(duì)象被創(chuàng)建了,而且相互引用,這樣創(chuàng)建了一個(gè)循環(huán)引用。它們會(huì)在函數(shù)調(diào)用后超出作用域,應(yīng)該可以釋放。然而引用計(jì)數(shù)算法考慮到2個(gè)對(duì)象中的每一個(gè)至少被引用了一次,因此都不可以被回收。
- function f() {
 - var o1 = {};
 - var o2 = {};
 - o1.p = o2; // o1 引用 o2
 - o2.p = o1; // o2 引用 o1\. 形成循環(huán)引用
 - }
 - f();
 
標(biāo)記清除算法
為了決定一個(gè)對(duì)象是否被需要,這個(gè)算法用于確定是否可以找到某個(gè)對(duì)象。
這個(gè)算法包含以下步驟。
- 垃圾回收器生成一個(gè)根列表。根通常是將引用保存在代碼中的全局變量。在JavaScript中,window對(duì)象是一個(gè)可以作為根的全局變量。
 - 所有的根都被檢查和標(biāo)記成活躍的(不是垃圾),所有的子變量也被遞歸檢查。所有可能從根元素到達(dá)的都不被認(rèn)為是垃圾。
 - 所有沒(méi)有被標(biāo)記成活躍的內(nèi)存都被認(rèn)為是垃圾。垃圾回收器就可以釋放內(nèi)存并且把內(nèi)存還給操作系統(tǒng)。
 
上圖就是標(biāo)記清除示意。
這個(gè)算法就比之前的(引用計(jì)算)要好些,因?yàn)?ldquo;一個(gè)對(duì)象沒(méi)有被引用”導(dǎo)致這個(gè)對(duì)象不能被訪問(wèn)。相反,正如我們?cè)谘h(huán)引用的示例中看到的,對(duì)象不能被訪問(wèn)到,不一定不存在引用。
2012年起,所有瀏覽器都內(nèi)置了標(biāo)記清除垃圾回收器。在過(guò)去幾年中,JavaScript垃圾回收領(lǐng)域中的所有改進(jìn)(代/增量/并行/并行垃圾收集)都是由這個(gè)算法(標(biāo)記清除法)改進(jìn)實(shí)現(xiàn)的,但并不是對(duì)垃圾收集算法本身的改進(jìn),也沒(méi)有改變它確定對(duì)象是否可達(dá)這個(gè)目標(biāo)。
推薦一篇文章,其中有關(guān)于跟蹤垃圾回收的細(xì)節(jié),包括了標(biāo)記清除法和它的優(yōu)化算法。
循環(huán)引用不再是問(wèn)題
在上面的例子中(循環(huán)引用的那個(gè)),在函數(shù)執(zhí)行完之后,這個(gè)2個(gè)對(duì)象沒(méi)有被任何可以到達(dá)的全局對(duì)象所引用。因此,他們將會(huì)被垃圾回收器發(fā)現(xiàn)為不可到達(dá)的。
盡管在這兩個(gè)對(duì)象之間有相互引用,但是他們不能從全局對(duì)象上到達(dá)。
垃圾回收器的反常行為
盡管垃圾回收器很方便,但是他們有一套自己的方案。其中之一就是不確定性。換句話說(shuō),GC是不可預(yù)測(cè)的。你不可能知道一個(gè)回收器什么時(shí)候會(huì)被執(zhí)行。這意味著程序在某些情況下會(huì)使用比實(shí)際需求還要多的內(nèi)存。在其他情況下,在特別敏感的應(yīng)用程序中,可能會(huì)出現(xiàn)短停頓。盡管不確定意味著不能確定回收工作何時(shí)執(zhí)行,但大多數(shù)GC實(shí)現(xiàn)都會(huì)在分配內(nèi)存的期間啟動(dòng)收集例程。如果沒(méi)有內(nèi)存分配,大部分垃圾回收就保持空閑。參考下面的情況。
- 執(zhí)行相當(dāng)大的一組分配。
 - 這些元素中的大部分(或者所有的)都被標(biāo)記為不可到達(dá)的(假設(shè)我們清空了一個(gè)指向我們不再需要的緩存的引用。)
 - 沒(méi)有更多的分配被執(zhí)行。
 
在這種情況下,大多數(shù)垃圾回收實(shí)現(xiàn)都不會(huì)做進(jìn)一步的回收。換句話說(shuō),盡管這里有不可達(dá)的引用變量可供回收,回收器也不會(huì)管。嚴(yán)格講,這不是泄露,但結(jié)果卻會(huì)占用比通常情況下更多的內(nèi)存。
什么是內(nèi)存泄漏
內(nèi)存泄漏基本上就是不再被應(yīng)用需要的內(nèi)存,由于某種原因,沒(méi)有被歸還給操作系統(tǒng)或者進(jìn)入可用內(nèi)存池。
編程語(yǔ)言喜歡不同的管理內(nèi)存方式。然而,一段確定的內(nèi)存是否被使用是一個(gè)不可判斷的問(wèn)題。換句話說(shuō),只有開(kāi)發(fā)者才能弄清楚,是否一段內(nèi)存可以被還給操作系統(tǒng)。
某些編程語(yǔ)言為開(kāi)發(fā)者提供了釋放內(nèi)存功能。另一些則期待開(kāi)發(fā)者清楚的知道一段內(nèi)存什么時(shí)候是沒(méi)用的。Wikipedia有一篇非常好的關(guān)于內(nèi)存管理的文章。
4種常見(jiàn)的JavaScript內(nèi)存泄漏
1:全局變量
JavaScript用一個(gè)有趣的方式管理未被聲明的變量:對(duì)未聲明的變量的引用在全局對(duì)象里創(chuàng)建一個(gè)新的變量。在瀏覽器的情況下,這個(gè)全局對(duì)象是window。換句話說(shuō):
- function foo(arg) {
 - bar = "some text";
 - }
 
等同于
- function foo(arg) {
 - window.bar = "some text";
 - }
 
如果bar被假定只在foo函數(shù)的作用域里引用變量,但是你忘記了使用var去聲明它,一個(gè)意外的全局變量就被聲明了。
在這個(gè)例子里,泄漏一個(gè)簡(jiǎn)單的字符串不會(huì)造成很大的傷害,但是它確實(shí)有可能變得更糟。
另外一個(gè)意外創(chuàng)建全局變量的方法是通過(guò)this:
- function foo() {
 - this.var1 = "potential accidental global";
 - }
 - // Foo作為函數(shù)調(diào)用,this指向全局變量(window)
 - // 而不是undefined
 - foo();
 
為了防止這些問(wèn)題發(fā)生,可以在你的JaveScript文件開(kāi)頭使用'use strict';。這個(gè)可以使用一種嚴(yán)格的模式解析JavaScript來(lái)阻止意外的全局變量。
除了意外創(chuàng)建的全局變量,明確創(chuàng)建的全局變量同樣也很多。這些當(dāng)然屬于不能被回收的(除非被指定為null或者重新分配)。特別那些用于暫時(shí)存儲(chǔ)數(shù)據(jù)的全局變量,是非常重要的。如果你必須要使用全局變量來(lái)存儲(chǔ)大量數(shù)據(jù),確保在是使用完成之后為其賦值null或者重新賦其他值。
2: 被遺忘的定時(shí)器或者回調(diào)
在JavaScript中使用setInterval是十分常見(jiàn)的。
大多數(shù)庫(kù),特別是提供觀察器或其他接收回調(diào)的實(shí)用函數(shù)的,都會(huì)在自己的實(shí)例無(wú)法訪問(wèn)前把這些回調(diào)也設(shè)置為無(wú)法訪問(wèn)。但涉及setInterval時(shí),下面這樣的代碼十分常見(jiàn):
- var serverData = loadData();
 - setInterval(function() {
 - var renderer = document.getElementById('renderer');
 - if(renderer) {
 - renderer.innerHTML = JSON.stringify(serverData);
 - }
 - }, 5000); //每5秒執(zhí)行一次
 
定時(shí)器可能會(huì)導(dǎo)致對(duì)不需要的節(jié)點(diǎn)或者數(shù)據(jù)的引用。
renderer對(duì)象在將來(lái)有可能被移除,讓interval處理器內(nèi)部的整個(gè)塊都變得沒(méi)有用。但由于interval仍然起作用,處理程序并不能被回收(除非interval停止)。如果interval不能被回收,它的依賴也不可能被回收。這就意味著serverData,大概保存了大量的數(shù)據(jù),也不可能被回收。
在觀察者的情況下,在他們不再被需要(或相關(guān)對(duì)象需要設(shè)置成不能到達(dá))的時(shí)候明確的調(diào)用移除是非常重要的。
在過(guò)去,這一點(diǎn)尤其重要,因?yàn)槟承g覽器(舊的IE6)不能很好的管理循環(huán)引用(更多信息見(jiàn)下文)。如今,大部分的瀏覽器都能而且會(huì)在對(duì)象變得不可到達(dá)的時(shí)候回收觀察處理器,即使監(jiān)聽(tīng)器沒(méi)有被明確的移除掉。然而,在對(duì)象被處理之前,要顯式地刪除這些觀察者仍然是值得提倡的做法。例如:
- var element = document.getElementById('launch-button');
 - var counter = 0;
 - function onClick(event) {
 - counter++;
 - element.innerHtml = 'text ' + counter;
 - }
 - element.addEventListener('click', onClick);
 - // 做點(diǎn)事
 - element.removeEventListener('click', onClick);
 - element.parentNode.removeChild(element);
 - // 當(dāng)元素被銷毀
 - //元素和事件都會(huì)即使在老的瀏覽器里也會(huì)被回收
 
如今的瀏覽器(包括IE和Edge)使用現(xiàn)代的垃圾回收算法,可以立即發(fā)現(xiàn)并處理這些循環(huán)引用。換句話說(shuō),先調(diào)用removeEventListener再刪節(jié)點(diǎn)并非嚴(yán)格必要。
jQuery等框架和插件會(huì)在丟棄節(jié)點(diǎn)前刪除監(jiān)聽(tīng)器。這都是它們內(nèi)部處理,以保證不會(huì)產(chǎn)生內(nèi)存泄漏,甚至是在有問(wèn)題的瀏覽器(沒(méi)錯(cuò),IE6)上也不會(huì)。
3: 閉包
閉包是JavaScript開(kāi)發(fā)的一個(gè)關(guān)鍵方面:一個(gè)內(nèi)部函數(shù)使用了外部(封閉)函數(shù)的變量。由于JavaScript運(yùn)行時(shí)實(shí)現(xiàn)的不同,它可能以下面的方式造成內(nèi)存泄漏:
- 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被調(diào)用,theThing獲得一個(gè)包含大數(shù)組和新的閉包(someMethod)的對(duì)象。同時(shí),變量unused保持了一個(gè)引用originalThing(theThing是上次調(diào)用replaceThing生成的值)的閉包。已經(jīng)有點(diǎn)困惑了吧?最重要的事情是一旦為同一父域中的作用域產(chǎn)生閉包,則該作用域是共享的。
這里,作用域產(chǎn)生了閉包,someMethod和unused共享這個(gè)閉包中的內(nèi)存。unused引用了originalThing。盡管unused不會(huì)被使用,someMethod可以通過(guò)theThing來(lái)使用replaceThing作用域外的變量(例如某些全局的)。而且someMethod和unused有共同的閉包作用域,unused對(duì)originalThing的引用強(qiáng)制oriiginalThing保持激活狀態(tài)(兩個(gè)閉包共享整個(gè)作用域)。這阻止了它的回收。
當(dāng)這段代碼重復(fù)執(zhí)行,可以觀察到被使用的內(nèi)存在持續(xù)增加。垃圾回收運(yùn)行的時(shí)候也不會(huì)變小。從本質(zhì)上來(lái)說(shuō),閉包的連接列表已經(jīng)創(chuàng)建了(以theThing變量為根),這些閉包每個(gè)作用域都間接引用了大數(shù)組,導(dǎo)致大量的內(nèi)存泄漏。
這個(gè)問(wèn)題被Meteor團(tuán)隊(duì)發(fā)現(xiàn),他們有一篇非常好的文章描述了閉包大量的細(xì)節(jié)。
4: DOM外引用
有的時(shí)候在數(shù)據(jù)結(jié)構(gòu)里存儲(chǔ)DOM節(jié)點(diǎn)是非常有用的,比如你想要快速更新一個(gè)表格幾行的內(nèi)容。此時(shí)存儲(chǔ)每一行的DOM節(jié)點(diǎn)的引用在一個(gè)字典或者數(shù)組里是有意義的。此時(shí)一個(gè)DOM節(jié)點(diǎn)有兩個(gè)引用:一個(gè)在dom樹(shù)中,另外一個(gè)在字典中。如果在未來(lái)的某個(gè)時(shí)候你想要去移除這些排,你需要確保兩個(gè)引用都不可到達(dá)。
- var elements = {
 - button: document.getElementById('button'),
 - image: document.getElementById('image')
 - };
 - function doStuff() {
 - image.src = 'http://example.com/image_name.png';
 - }
 - function removeImage() {
 - //image是body元素的子節(jié)點(diǎn)
 - document.body.removeChild(document.getElementById('image'));
 - //這個(gè)時(shí)候我們?cè)谌值膃lements對(duì)象里仍然有一個(gè)對(duì)#button的引用。
 - //換句話說(shuō),buttom元素仍然在內(nèi)存中而且不能被回收。
 - }
 
當(dāng)涉及DOM樹(shù)內(nèi)部或子節(jié)點(diǎn)時(shí),需要考慮額外的考慮因素。例如,你在JavaScript中保持對(duì)某個(gè)表的特定單元格的引用。有一天你決定從DOM中移除表格但是保留了對(duì)單元格的引用。人們也許會(huì)認(rèn)為除了單元格其他的都會(huì)被回收。實(shí)際并不是這樣的:?jiǎn)卧袷潜砀竦囊粋€(gè)子節(jié)點(diǎn),子節(jié)點(diǎn)保持了對(duì)父節(jié)點(diǎn)的引用。確切的說(shuō),JS代碼中對(duì)單元格的引用造成了整個(gè)表格被留在內(nèi)存中了,所以在移除有被引用的節(jié)點(diǎn)時(shí)候要當(dāng)心。
我們?cè)赟essionStack努力遵循這些***實(shí)踐,因?yàn)椋?/p>
一旦你整合essionStack到你的生產(chǎn)應(yīng)用中,它就開(kāi)始記錄所有的事情:DOM變化、用戶交互、JS異常、堆棧跟蹤、失敗的網(wǎng)絡(luò)請(qǐng)求、調(diào)試信息,等等。
通過(guò)SessionStack,你可以回放應(yīng)用中的問(wèn)題,看到問(wèn)題對(duì)用戶的影響。所有這些都不會(huì)對(duì)你的應(yīng)用產(chǎn)生性能的影響。因?yàn)橛脩艨梢灾匦录虞d頁(yè)面或者在應(yīng)用中跳轉(zhuǎn),所有的觀察者、攔截器、變量分配都必須合理處置。以免造成內(nèi)存泄漏,也預(yù)防增加整個(gè)應(yīng)用的內(nèi)存占用。
這是一個(gè)免費(fèi)的計(jì)劃,你現(xiàn)在可以嘗試一下。






















 
 
 











 
 
 
 