摘要:本系列的第一篇文章著重提供一個(gè)關(guān)于引擎運(yùn)行時(shí)和調(diào)用棧的概述。在硬件層面,計(jì)算機(jī)內(nèi)存由大量的觸發(fā)器組成。每個(gè)觸發(fā)器包含幾個(gè)晶體管能夠存儲(chǔ)一個(gè)比特譯注位??梢酝ㄟ^唯一標(biāo)識(shí)符來訪問單個(gè)觸發(fā)器,所以可以對(duì)它們進(jìn)行讀寫操作。比特稱為個(gè)字節(jié)。
原文 How JavaScript works: memory management + how to handle 4 common memory leaks
幾周前我們開始了一個(gè)系列博文旨在深入挖掘 JavaScript 并弄清楚它的工作原理:我們認(rèn)為通過了解 JavaScript 的構(gòu)建單元并熟悉它們是怎樣結(jié)合起來的,有助于寫出更好的代碼和應(yīng)用。
本系列的第一篇文章著重提供一個(gè)關(guān)于引擎、運(yùn)行時(shí)和調(diào)用棧的概述。第二篇文章深入分析了 Google 的 V8 引擎的內(nèi)部實(shí)現(xiàn)并提供了一些編寫更優(yōu)質(zhì) JavaScript 代碼的建議。
在第三篇的本文中,我們將會(huì)討論另一個(gè)非常重要的主題,由于日常使用的編程語言的逐漸成熟和復(fù)雜性,它被越來越多的開發(fā)者忽視——內(nèi)存管理。我們還會(huì)提供一些在 SessionStack 中遵循的關(guān)于如何處理 JavaScript 內(nèi)存泄露的方法,我們必須保證 SessionStack 不會(huì)發(fā)生內(nèi)存泄漏,或?qū)е抡线M(jìn)來的應(yīng)用增加內(nèi)存消耗。
概述像 C 這樣的語言,具有低水平的內(nèi)存管理原語如 malloc() 和 free(),這些原語被開發(fā)者用來顯式地向操作系統(tǒng)分配和釋放內(nèi)存。
同時(shí),JavaScript 在事物(對(duì)象、字符串等)被創(chuàng)建時(shí)分配內(nèi)存,并在它們不再需要用到時(shí)自動(dòng)釋放內(nèi)存,這個(gè)過程稱為垃圾收集。這個(gè)看似自動(dòng)釋放資源的特性是困惑的來源,造成 JavaScript(和其他高級(jí)語言)開發(fā)者錯(cuò)誤的印象,認(rèn)為他們可以選擇不必關(guān)心內(nèi)存管理。這是個(gè)天大的誤解。
即便在使用高級(jí)編程語言時(shí),開發(fā)者也應(yīng)該了解內(nèi)存管理(至少最基本的)。有時(shí)會(huì)遇到自動(dòng)內(nèi)存管理的問題(如垃圾收集器的BUG和實(shí)現(xiàn)限制等),開發(fā)者應(yīng)該了解這些問題才能合理地處理它們(或找到適當(dāng)?shù)慕鉀Q方案,用最小的代價(jià)和代碼債)。
內(nèi)存生命周期無論使用哪種編程語言,內(nèi)存的生命周期幾乎總是相同的:
下面是周期中每個(gè)步驟發(fā)生了什么的概覽:
分配內(nèi)存——內(nèi)存由允許程序使用的操作系統(tǒng)分配。在低級(jí)編程語言(如 C)中這是一個(gè)作為開發(fā)人員應(yīng)該處理的顯式操作。而在高級(jí)編程語言中是由語言本身幫你處理的。
使用內(nèi)存——這是程序?qū)嶋H上使用之前所分配內(nèi)存的階段。讀寫操作發(fā)生在使用代碼中分配的變量時(shí)。
釋放內(nèi)存——現(xiàn)在是釋放不需要的整個(gè)內(nèi)存的時(shí)候了,這樣它才能變得空閑以便再次可用。與分配內(nèi)存一樣,在低級(jí)編程語言中這是一個(gè)顯式操作。
想要快速瀏覽調(diào)用棧和內(nèi)存堆的概念,可以閱讀我們關(guān)于這個(gè)主題的第一篇文章。
什么是內(nèi)存?在直接介紹 JavaScript 中的內(nèi)存之前,我們會(huì)簡(jiǎn)要討論一下內(nèi)存是什么及它是怎樣工作的。
在硬件層面,計(jì)算機(jī)內(nèi)存由大量的觸發(fā)器組成。每個(gè)觸發(fā)器包含幾個(gè)晶體管能夠存儲(chǔ)一個(gè)比特(譯注:1位)。可以通過唯一標(biāo)識(shí)符來訪問單個(gè)觸發(fā)器,所以可以對(duì)它們進(jìn)行讀寫操作。因此從概念上,我們可以把整個(gè)計(jì)算機(jī)內(nèi)存想象成一個(gè)巨大的可讀寫的比特陣列。
作為人類,我們并不擅長(zhǎng)使用字節(jié)進(jìn)行所有的思考和算術(shù),我們把它們組織成更大的組合,一起用來表示數(shù)字。8比特稱為1個(gè)字節(jié)。除字節(jié)之外,還有其他詞(有時(shí)是16比特、有時(shí)是32比特)。
很多東西存儲(chǔ)在內(nèi)存中:
所有程序使用的所有變量和其他數(shù)據(jù)。
程序代碼,包括操作系統(tǒng)的。
編譯器和操作系統(tǒng)一起工作來處理大部分的內(nèi)存管理,但我們還是建議你了解一下底層發(fā)生的事情。
編譯代碼時(shí),編譯器可以檢測(cè)到原始數(shù)據(jù)類型然后提前計(jì)算出需要多少內(nèi)存。隨后給棧空間中的程序分配所需額度。分配變量的空間被稱為??臻g是因?yàn)楫?dāng)函數(shù)調(diào)用時(shí),它們被添加到已有內(nèi)存的頂部。當(dāng)它們終止時(shí),根據(jù)后進(jìn)先出的原則被移除。例如,考慮如下聲明:
int n; // 4 bytes 4字節(jié) int x[4]; // array of 4 elements, each 4 bytes 含有四個(gè)元素的數(shù)組,每個(gè)4字節(jié) double m; // 8 bytes 8字節(jié)
編譯器能夠立即看出這段代碼需要4+4*4+8=28字節(jié)。
這是現(xiàn)今處理整型和雙精度浮點(diǎn)數(shù)的大小。20年以前,整型通常是2字節(jié),雙精度是4字節(jié)。代碼永遠(yuǎn)不應(yīng)該依賴當(dāng)前基本數(shù)據(jù)類型的大小。
編譯器將會(huì)插入代碼與操作系統(tǒng)交互,請(qǐng)求棧上存儲(chǔ)變量所需的字節(jié)數(shù)。
在上面的例子中,編譯器知道每個(gè)變量的精確內(nèi)存地址。實(shí)際上,每當(dāng)寫入變量 n,它都會(huì)在內(nèi)部被轉(zhuǎn)換成類似“內(nèi)存地址4127963”的東西。
注意,如果試圖在這里訪問 x[4],將會(huì)訪問到與 m 關(guān)聯(lián)的數(shù)據(jù)。這是因?yàn)槲覀冊(cè)谠L問數(shù)組中一個(gè)不存在的元素——比數(shù)組中最后實(shí)際分配的成員 x[3] 要遠(yuǎn)4個(gè)字節(jié),這可能最終會(huì)讀取(或?qū)懭耄┮恍?m 中的比特。這必將會(huì)使程序其余部分產(chǎn)生非常不希望得到的結(jié)果。
當(dāng)函數(shù)調(diào)用其他函數(shù)時(shí),每個(gè)函數(shù)都會(huì)在被調(diào)用時(shí)得到屬于自己的一塊棧。這里不僅保存了所有的局部變量,還保存著記錄執(zhí)行位置的程序計(jì)數(shù)器。當(dāng)函數(shù)結(jié)束時(shí),它的內(nèi)存單元再次變得空閑可供他用。
動(dòng)態(tài)分配不幸的是,當(dāng)我們?cè)诰幾g時(shí)無法得知變量需要多少內(nèi)存的時(shí)候事情就沒那么簡(jiǎn)單了。假設(shè)我們要做如下的事情:
int n = readInput(); // reads input from the user ... // create an array with "n" elements
這在編譯時(shí),編譯器無法知道數(shù)組需要多少內(nèi)存,因?yàn)樗Q于用戶提供的值。
因此無法為棧中的變量分配空間。相反,我們的程序需要在運(yùn)行時(shí)顯式向操作系統(tǒng)請(qǐng)求合適的空間。這種內(nèi)存由堆空間分配。靜態(tài)和動(dòng)態(tài)內(nèi)存分配的區(qū)別總結(jié)為下表:
要充分理解動(dòng)態(tài)內(nèi)存分配的原理,我們需要在指針上多花些時(shí)間,但這已經(jīng)偏離了本文的主題。如果有興趣學(xué)習(xí)更多,請(qǐng)?jiān)谠u(píng)論里留言告訴我們,我們可以在以后的文章中討論更多關(guān)于指針的細(xì)節(jié)。
JavaScript 中的分配現(xiàn)在我們將解釋第一步(分配內(nèi)存)如何在 JavaScript 中工作。
JavaScript 將開發(fā)者從內(nèi)存分配的責(zé)任中解放出來——在聲明變量的同時(shí)它會(huì)自己處理內(nèi)存分配。
var n = 374; // allocates memory for a number 為數(shù)值分配內(nèi)存 var s = "sessionstack"; // allocates memory for a string 為字符串分配內(nèi)存 var o = { a: 1, b: null }; // allocates memory for an object and its contained values 為對(duì)象及其包含的值分配內(nèi)存 var a = [1, null, "str"]; // (like object) allocates memory for the // array and its contained values (與對(duì)象一樣)為數(shù)組及其包含的值分配內(nèi)存 function f(a) { return a + 3; } // allocates a function (which is a callable object) 分配函數(shù)(即可調(diào)用對(duì)象) // function expressions also allocate an object 函數(shù)表達(dá)式同樣分配一個(gè)對(duì)象 someElement.addEventListener("click", function() { someElement.style.backgroundColor = "blue"; }, false);
某些函數(shù)調(diào)用也產(chǎn)生對(duì)象分配:
var d = new Date(); // allocates a Date object 分配一個(gè)日期對(duì)象 var e = document.createElement("div"); // allocates a DOM element 分配一個(gè)DOM元素
方法可以分配新的值或?qū)ο螅?/p>
var s1 = "sessionstack"; var s2 = s1.substr(0, 3); // s2 is a new string s2是一個(gè)新字符串 // Since strings are immutable, 由于字符串是不可變的 // JavaScript may decide to not allocate memory, JavaScript可能會(huì)決定不分配內(nèi)存 // but just store the [0, 3] range. 而僅僅存儲(chǔ)[0, 3]這個(gè)范圍 var a1 = ["str1", "str2"]; var a2 = ["str3", "str4"]; var a3 = a1.concat(a2); // new array with 4 elements being 含有四個(gè)元素的數(shù)組 // the concatenation of a1 and a2 elements 由a1和a2的元素的結(jié)合在 JavaScript 中使用內(nèi)存
在 JavaScript 中使用分配的內(nèi)存基本上意味著在其中進(jìn)行讀寫操作。
這可以通過讀取或?qū)懭胱兞康闹祷驅(qū)ο髮傩?、甚至向函?shù)傳參數(shù)的時(shí)候?qū)崿F(xiàn)。
在不需要內(nèi)存時(shí)將其釋放大多數(shù)內(nèi)存管理問題出現(xiàn)在這個(gè)階段。
最大的難題是弄清楚何時(shí)不再需要分配的內(nèi)存。通常需要開發(fā)者來決定這塊內(nèi)存在程序的何處不再需要并且釋放它。
高級(jí)編程語言嵌入了一個(gè)叫做垃圾收集器軟件,它的工作是追蹤內(nèi)存分配和使用以便發(fā)現(xiàn)分配的內(nèi)存何時(shí)不再需要,并在這種情況下自動(dòng)釋放它。
不幸的是這個(gè)過程只是個(gè)近似的過程,因?yàn)橹朗欠襁€需要一些內(nèi)存的一般問題是不可決定的(無法靠算法解決)。
大多數(shù)垃圾收集器的工作原理是收集不能再訪問的內(nèi)存,比如指向它的所有變量都超出作用域。但這也是對(duì)可收集內(nèi)存空間的一種低估,因?yàn)樵谌魏螘r(shí)候作用域內(nèi)都仍可能有一個(gè)變量指向一個(gè)內(nèi)存地址,然而它再也不會(huì)被訪問。
垃圾收集由于無法確定某些內(nèi)存是否“不再需要”,垃圾收集實(shí)現(xiàn)了對(duì)一般解決方法的限制。這一節(jié)將會(huì)解釋理解主要的垃圾收集算法的必要概念和局限性。
內(nèi)存引用垃圾收集算法依賴的主要概念之一是引用。
在內(nèi)存管理的上下文中,如果一個(gè)對(duì)象可以訪問另一個(gè)對(duì)象則說成是前者引用了后者(可是隱式也可是顯式)。例如,JavaScript 對(duì)象有對(duì)其原型的引用(隱式引用)和對(duì)屬性的引用(顯式引用)。
在這個(gè)上下文中,”對(duì)象“的概念擴(kuò)展到比常規(guī) JavaScript 對(duì)象更廣泛的范圍,并且還包含函數(shù)作用域(或全局詞法作用域)。
詞法作用域規(guī)定了如何解析嵌套函數(shù)中的變量名稱:內(nèi)層函數(shù)包含了父函數(shù)的作用域,即使父函數(shù)已返回。引用計(jì)數(shù)垃圾收集
這是最簡(jiǎn)單的垃圾收集算法。如果沒有指向?qū)ο蟮囊?,就被認(rèn)為是“可收集的”。
看看如下代碼:
var o1 = { o2: { x: 1 } }; // 2 objects are created. // "o2" is referenced by "o1" object as one of its properties. // None can be garbage-collected // 創(chuàng)建了兩個(gè)對(duì)象 // o2 被當(dāng)作 o1 的屬性而引用 // 現(xiàn)在沒有可被收集的垃圾 var o3 = o1; // the "o3" variable is the second thing that // has a reference to the object pointed by "o1". // o3是第二個(gè)引用了o1 所指向?qū)ο蟮淖兞俊? o1 = 1; // now, the object that was originally in "o1" has a // single reference, embodied by the "o3" variable // 現(xiàn)在,本來被 o1 指向的對(duì)象變成了單一引用,體現(xiàn)在 o3 上。 var o4 = o3.o2; // reference to "o2" property of the object. // This object has now 2 references: one as // a property. // The other as the "o4" variable // 通過屬性 o2 建立了對(duì)它所指對(duì)象的引用 // 這個(gè)對(duì)象現(xiàn)在有兩個(gè)引用:一個(gè)作為屬性的o2 // 另一個(gè)是變量 o4 o3 = "374"; // The object that was originally in "o1" has now zero // references to it. // It can be garbage-collected. // However, what was its "o2" property is still // referenced by the "o4" variable, so it cannot be // freed. // 原本由 o1 引用的對(duì)象現(xiàn)在含有0個(gè)引用。 // 它可以被作為垃圾而收集 // 但是它的屬性 o2 仍然被變量 o4 引用,所以它不能被釋放。 o4 = null; // what was the "o2" property of the object originally in // "o1" has zero references to it. // It can be garbage collected. // 原本由 o1 引用的對(duì)象的屬性 o2 現(xiàn)在也只有0個(gè)引用,它現(xiàn)在可以被收集了。循環(huán)制造出問題
這在循環(huán)引用時(shí)存在限制。在下面示例中,創(chuàng)建了兩個(gè)互相引用的對(duì)象,從而創(chuàng)建了一個(gè)循環(huán)。它們?cè)诤瘮?shù)調(diào)用返回后超出作用域,所以實(shí)際上它們已經(jīng)沒用了并應(yīng)該被釋放。但引用計(jì)數(shù)算法考慮到由于它們至少被引用了一次,所以兩者都不會(huì)被當(dāng)作垃圾收集。
function f() { var o1 = {}; var o2 = {}; o1.p = o2; // o1 references o2 o2.p = o1; // o2 references o1. This creates a cycle. } f();標(biāo)記和清理算法
為了決定是否還需要對(duì)象,這個(gè)算法確定了對(duì)象是否可以訪問。
標(biāo)記和清理算法有如下三個(gè)步驟:
根:通常,根是被代碼引用的全局變量。例如在 JavaScript 中,可以作為根的全局變量是 window 對(duì)象。同一對(duì)象在 Node.js 中被稱為 global。垃圾收集器建立了所有根的完整列表。
接著算法檢查所有根及它們的子節(jié)點(diǎn),并把它們標(biāo)記為活躍的(意為它們不是垃圾)。根所不能獲取到的任何東西都被標(biāo)記為垃圾。
最終,垃圾收集器把未標(biāo)記為活躍的所有內(nèi)存片段釋放并返還給操作系統(tǒng)。
這個(gè)算法比之前的更好,因?yàn)椤耙粋€(gè)對(duì)象沒有引用”造成這個(gè)對(duì)象變得不可獲取,但通過循環(huán)我們看到反過來卻是不成立的。
2012年后,所有現(xiàn)代瀏覽器都裝載了標(biāo)記和清理垃圾收集器。近年來,在 JavaScript 垃圾收集所有領(lǐng)域的改善(分代/增量/并發(fā)/并行垃圾收集)都是這個(gè)算法(標(biāo)記和清理)的實(shí)現(xiàn)改進(jìn),既不是垃圾收集算法自身的改進(jìn)也并非決定是否對(duì)象可獲取的目標(biāo)的改進(jìn)。
在這篇文章中,你可以閱讀到有關(guān)追蹤垃圾收集的大量細(xì)節(jié),并且涵蓋了標(biāo)記和清理及它的優(yōu)化。
循環(huán)不再是問題在上面的第一個(gè)例子中,當(dāng)函數(shù)調(diào)用返回后,兩個(gè)對(duì)象不再被全局對(duì)象的可獲取節(jié)點(diǎn)引用。結(jié)果是,它們會(huì)被垃圾收集齊認(rèn)為是不可獲取的。
即便它們彼此間仍存在引用,它們也不能被根獲取到。
垃圾收集器與直覺相反的行為雖然垃圾收集器很方便,但它們也有自己的一套折中策略。其一是非確定性。換句話說,垃圾收集是不可預(yù)測(cè)的。你無法確切知道垃圾收集什么時(shí)候執(zhí)行。這意味著在一些情況下程序會(huì)要求比實(shí)際需要更多的內(nèi)存。另一些情況下,短時(shí)暫停會(huì)在一些特別敏感的應(yīng)用中很明顯。雖然非確定性意味著無法確定垃圾收集執(zhí)行的時(shí)間,但大多數(shù)垃圾收集的實(shí)現(xiàn)都共享一個(gè)通用模式:在內(nèi)存分配期間進(jìn)行收集。如果沒有內(nèi)存分配發(fā)生,垃圾收集器就處于閑置??紤]以下場(chǎng)景:
執(zhí)行大量?jī)?nèi)存分配。
它們大多數(shù)(或全部)被標(biāo)記為不可獲?。僭O(shè)我們將一個(gè)不再需要的指向緩存的引用置為null)。
不再有進(jìn)一步的內(nèi)存分配發(fā)生。
在這個(gè)場(chǎng)景下,大多數(shù)垃圾收集不會(huì)再運(yùn)行收集傳遞。換言之,即時(shí)存在無法訪問的引用可以收集,它們也不會(huì)被收集器注意到。這些不是嚴(yán)格意義上的泄露,但是仍然導(dǎo)致了比正常更高的內(nèi)存使用。
什么是內(nèi)存泄露?就像內(nèi)存所暗示的,內(nèi)存泄露是被應(yīng)用使用過的一塊內(nèi)存在不需要時(shí)尚未返還給操作操作系統(tǒng)或由于糟糕的內(nèi)存釋放未能返還。
編程語言喜歡用不同的方式進(jìn)行內(nèi)存管理。但一塊已知內(nèi)存是否還被使用實(shí)際上是個(gè)無法決定的問題。換句話說,只有開發(fā)人員可以弄清除是否應(yīng)該將一塊內(nèi)存還給操作系統(tǒng)。
某些編程語言提供了開發(fā)人員手動(dòng)釋放內(nèi)存的特性。另一些則希望由開發(fā)人員完全提供顯式的聲明。維基百科上有關(guān)于手動(dòng)和自動(dòng)內(nèi)存管理的好的文章。
四種常見 JavaScript 泄露 1:全局變量JavaScript 處理未聲明變量的方式很有趣:當(dāng)引用一個(gè)還未聲明的變量時(shí),就在全局對(duì)象上創(chuàng)建一個(gè)新變量。在瀏覽器中,全局對(duì)象是 window,這意味著:
function foo(arg) { bar = "some text"; }
等價(jià)于
function foo(arg) { window.bar = "some text"; }
讓我們假設(shè) bar 僅是為了在函數(shù) foo 中引用變量。但如果不使用 var 聲明,將創(chuàng)建一個(gè)多余的全局變量。在上面的例子中,并不會(huì)引起多大損害。但你仍可想到一個(gè)更具破壞性的場(chǎng)景。
你可以偶然地通過 this 創(chuàng)建一個(gè)全局變量:
function foo() { this.var1 = "potential accidental global"; } // Foo called on its own, this points to the global object (window) // rather than being undefined. foo();
可以通過在 JavaScript 文件的開頭添加 "use strict"; 來避免這一切,這會(huì)開啟一個(gè)更加嚴(yán)格的模式來解析代碼,它可以防止意外創(chuàng)建全局變量。
意外的全局變量當(dāng)然是個(gè)問題,但是通常情況下,你的代碼會(huì)被顯示全局變量污染,并且根據(jù)定義它們無法被垃圾收集器收集。應(yīng)該尤其注意用來臨時(shí)性存儲(chǔ)和處理大量信息的全局變量。如果你必須使用全局變量存儲(chǔ)信息而當(dāng)你這樣做了時(shí),確保一旦完成之后就將它賦值為 null 或重新分配。
2:被遺忘的計(jì)時(shí)器或回調(diào)讓我們來看看 setInterval 的列子,它在 JavaScript 中經(jīng)常用到。
提供觀察者模式的庫和其他接受回調(diào)函數(shù)的實(shí)現(xiàn)通常會(huì)在它們的實(shí)例無法獲取確保對(duì)這些回調(diào)函數(shù)的引用也變成無法獲取。同樣,下面的代碼不難找到:
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById("renderer"); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //This will be executed every ~5 seconds.
上面這段代碼展示了引用不再需要的節(jié)點(diǎn)或數(shù)據(jù)的后果。
renderer 對(duì)象可能在某個(gè)時(shí)候被覆蓋或移除,這將會(huì)導(dǎo)致封裝在間隔處理函數(shù)中的語句變得冗余。一旦發(fā)生這種情況,處理器和它依賴的東西必須要等到間隔器先被停止之后才能收集(記住,它依然是活躍的)。這將會(huì)導(dǎo)致這樣的事實(shí):用于儲(chǔ)存和處理數(shù)據(jù)的 serverData 也將不會(huì)被收集。
當(dāng)使用觀察者模式時(shí),你需要在完成后確保通過顯示調(diào)用移除它們(既不再需要觀察者,對(duì)象也變成不可獲取的)。
幸運(yùn)的是,大多數(shù)現(xiàn)代瀏覽器會(huì)為我們處理好這些事務(wù):它們會(huì)自動(dòng)收集被觀察對(duì)象變成不可獲取的觀察者處理器,即使你忘記移除這些監(jiān)聽器。過去一些瀏覽器是無法做到這些的(老IE6)。
不過,符合最佳實(shí)踐的還是在對(duì)象過時(shí)時(shí)移除觀察者。來看下面的例子:
var element = document.getElementById("launch-button"); var counter = 0; function onClick(event) { counter++; element.innerHtml = "text " + counter; } 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. // 現(xiàn)在,當(dāng)元素超出作用域之后, // 即使是不能很好處理循環(huán)的老瀏覽器也能將元素和點(diǎn)擊處理函數(shù)回收。
在使節(jié)點(diǎn)變成不可獲取之前不再需要調(diào)用 removeEventListener ,因?yàn)楝F(xiàn)代瀏覽器支持垃圾收集器可以探測(cè)這些循環(huán)并進(jìn)行適當(dāng)處理。
如果你利用 jQuery APIs(其他庫和框架也支持),它也可以在節(jié)點(diǎn)無效之前移除監(jiān)聽器。這個(gè)庫也會(huì)確保沒有內(nèi)存泄露發(fā)生,即使應(yīng)用運(yùn)行在老瀏覽器之下。
3:閉包JavaScript 開發(fā)的核心領(lǐng)域之一是閉包:內(nèi)層函數(shù)可以訪問外層(封閉)函數(shù)的變量。 歸咎于 JavaScript 運(yùn)行時(shí)的實(shí)現(xiàn)細(xì)節(jié),可能發(fā)生下面這樣的內(nèi)存泄露:
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) // a reference to "originalThing" console.log("hi"); }; theThing = { longStr: new Array(1000000).join("*"), someMethod: function () { console.log("message"); } }; }; setInterval(replaceThing, 1000);
當(dāng) replaceThing 調(diào)用后,theThing 被賦值為一個(gè)對(duì)象,由一個(gè)大數(shù)組和一個(gè)新的閉包(someMethod)組成。還有,originalThing 被變量 unused 擁有的閉包所引用(值是上一次 replaceThing 調(diào)用所得到的變量 theThing )。要記住的是當(dāng)一個(gè)閉包作用域被創(chuàng)建時(shí),位于同一個(gè)父作用域內(nèi)的其他閉包也共享這個(gè)作用域。
在這個(gè)案列中,為閉包 someMethod 創(chuàng)建的作用域被 unused 共享。即便 unused 從未使用,someMethod 可以通過位于 replaceThing 外層的 theThing 使用(例如,在全局中)。又因?yàn)?someMethod 與 unused 共享閉包作用域,unused 引用的 originalThing 被強(qiáng)制處于活躍狀態(tài)(在兩個(gè)閉包之間被共享的整個(gè)作用域)。這些妨礙了被收集。
在上述列子中,當(dāng) unused 引用了 originalThing 時(shí),共享了為 someMethod 創(chuàng)建的作用域??梢酝ㄟ^ replaceThing 作用域外的 theThing 使用 someMethod,且不管其實(shí) unused 從未使用。事實(shí)上 unused 引用了 originalThing 使其保持在活躍狀態(tài),因?yàn)?b>someMethod 與 unused 共享了閉包作用域。
所有的這些導(dǎo)致了相當(dāng)大的內(nèi)存泄露。你會(huì)看到在上述代碼一遍又一遍運(yùn)行時(shí)內(nèi)存使用量的激增。它不會(huì)在垃圾收集器運(yùn)行時(shí)變小。一系列的閉包被創(chuàng)建(此例中根是變量 theThing),每一個(gè)閉包作用域都間接引用了大數(shù)組。
Meteor 團(tuán)隊(duì)發(fā)現(xiàn)了這個(gè)問題,他們有一篇非常棒的文章詳細(xì)描述了這個(gè)問題。
4:外部DOM引用還有種情況是當(dāng)開發(fā)人員把 DOM 節(jié)點(diǎn)儲(chǔ)存在數(shù)據(jù)結(jié)構(gòu)里的時(shí)候。假設(shè)你想快速更新表格中某幾行的內(nèi)容。如果把對(duì)每行的 DOM 引用存在字典中或數(shù)組中,就會(huì)存在對(duì)相同 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() { // The image is a direct child of the body element. // 圖片是body的直接子元素 document.body.removeChild(document.getElementById("image")); // At this point, we still have a reference to #button in the //global elements object. In other words, the button element is //still in memory and cannot be collected by the GC. // 這時(shí),全局elements對(duì)象仍有一個(gè)對(duì)#button元素的引用。換句話說,button元素 // 仍然在內(nèi)存里,無法被垃圾收集器回收。 }
還有一個(gè)例外情況應(yīng)該被考慮到,它出現(xiàn)在引用 DOM 樹的內(nèi)部或葉節(jié)點(diǎn)時(shí)。如果你在代碼里保存了一個(gè)對(duì)表格單元(td 標(biāo)簽)的引用,然后決定把表格從 DOM 中移除但保留對(duì)那個(gè)特別單元格的引用,就能預(yù)料到將會(huì)有大量的內(nèi)存泄露。你可能認(rèn)為垃圾收集器將釋放其他所有的東西除了那個(gè)單元格。但是,這將不會(huì)發(fā)生。因?yàn)檫@個(gè)單元格是表格的一個(gè)子節(jié)點(diǎn),子節(jié)點(diǎn)保存了對(duì)它們父節(jié)點(diǎn)的引用,引用這一個(gè)單元格將會(huì)在內(nèi)存里保存整個(gè)表格。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/106376.html
摘要:是如何工作的內(nèi)存管理以及如何處理四種常見的內(nèi)存泄漏原文譯者幾個(gè)禮拜之前我們開始一系列對(duì)于以及其本質(zhì)工作原理的深入挖掘我們認(rèn)為通過了解的構(gòu)建方式以及它們是如何共同合作的,你就能夠?qū)懗龈玫拇a以及應(yīng)用。 JavaScript是如何工作的:內(nèi)存管理以及如何處理四種常見的內(nèi)存泄漏 原文:How JavaScript works: memory management + how to han...
摘要:本文作為第三篇,將會(huì)討論另一個(gè)開發(fā)者容易忽視的重要主題內(nèi)存管理。我們也會(huì)提供一些關(guān)于如何處理內(nèi)存泄露的技巧。這是當(dāng)前整型和雙精度的大小。然而,這是一組可以收集的內(nèi)存空間的近似值。 本文轉(zhuǎn)載自:眾成翻譯譯者:Leslie Wang審校: 為之漫筆鏈接:http://www.zcfy.cc/article/4211原文:https://blog.sessionstack.com/how-j...
摘要:這是因?yàn)槲覀冊(cè)L問了數(shù)組中不存在的數(shù)組元素它超過了最后一個(gè)實(shí)際分配到內(nèi)存的數(shù)組元素字節(jié),并且有可能會(huì)讀取或者覆寫的位。包含個(gè)元素的新數(shù)組由和數(shù)組元素所組成中的內(nèi)存使用中使用分配的內(nèi)存主要指的是內(nèi)存讀寫。 原文請(qǐng)查閱這里,本文有進(jìn)行刪減,文后增了些經(jīng)驗(yàn)總結(jié)。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第三章。 我們將會(huì)討論日常使用中另一個(gè)被開發(fā)...
閱讀 739·2021-11-18 10:02
閱讀 3670·2021-09-02 10:21
閱讀 1776·2021-08-27 16:16
閱讀 2091·2019-08-30 15:56
閱讀 2440·2019-08-29 16:53
閱讀 1399·2019-08-29 11:18
閱讀 2984·2019-08-26 10:33
閱讀 2668·2019-08-23 18:34