久久r热视频,国产午夜精品一区二区三区视频,亚洲精品自拍偷拍,欧美日韩精品二区

您的位置:首頁(yè)技術(shù)文章
文章詳情頁(yè)

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(nèi)存泄露

瀏覽:3日期:2023-11-12 09:05:32

幾周前,我們開始寫一個(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è)開發(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() 。開發(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ǔ)言)開發(fā)者誤以為可以不關(guān)心內(nèi)存管理。 這是一個(gè)很大的錯(cuò)誤

即使使用高級(jí)級(jí)語(yǔ)言,開發(fā)者也應(yīng)該對(duì)于內(nèi)存管理有一定的理解(至少有基本的理解)。有時(shí)自動(dòng)內(nèi)存管理存在一些問(wèn)題(例如垃圾回收實(shí)現(xiàn)可能存在缺陷或者不足),開發(fā)者必須弄明白這些問(wèn)題,以便找一個(gè)合適解決方法。

內(nèi)存生命周期

無(wú)論你用哪一種編程語(yǔ)言,內(nèi)存生命周期幾乎總是一樣的:

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(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è)作為開發(fā)者需要處理的顯式操作。在高級(jí)語(yǔ)言中,然而,這些操作都代替開發(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ā)電路都包含一些<span style='font-size: 1rem;'>能夠儲(chǔ)存1位數(shù)據(jù)的</span>晶體管。觸發(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)共同工作幫助開發(fā)者完成大部分的內(nèi)存管理,但是我們推薦你了解一下底層到底發(fā)生了什么。

編譯代碼的時(shí)候,編譯器會(huì)解析原始數(shù)據(jù)類型,提前計(jì)算出它們需要多大的內(nèi)存空間。然后將所需的數(shù)量分配在 棧空間 中。之所以稱為棧空間,是因在函數(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)存(開始聲明的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é)果。

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(nèi)存泄露

當(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):

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(nè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在開發(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)樽址遣豢勺兊?/ 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í)候不再被需要。這常常要求開發(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ū)ο蟮淖兞縪1 = 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();

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(nèi)存泄露

標(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)。

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(nèi)存泄露

上圖就是標(biāo)記清除示意。

這個(gè)算法就比之前的(引用計(jì)算)要好些,因?yàn)椤耙粋€(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á)的。

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(nèi)存泄露

盡管在這兩個(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)存池。

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(nèi)存泄露

編程語(yǔ)言喜歡不同的管理內(nèi)存方式。然而,一段確定的內(nèi)存是否被使用是一個(gè)不可判斷的問(wèn)題。換句話說(shuō),只有開發(fā)者才能弄清楚,是否一段內(nèi)存可以被還給操作系統(tǒng)。

某些編程語(yǔ)言為開發(fā)者提供了釋放內(nèi)存功能。另一些則期待開發(fā)者清楚的知道一段內(nèi)存什么時(shí)候是沒(méi)用的。Wikipedia有一篇非常好的關(guān)于內(nèi)存管理的文章。

4種常見的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)// 而不是undefinedfoo();

為了防止這些問(wèn)題發(fā)生,可以在你的JaveScript文件開頭使用 ’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 是十分常見的。

大多數(shù)庫(kù),特別是提供觀察器或其他接收回調(diào)的實(shí)用函數(shù)的,都會(huì)在自己的實(shí)例無(wú)法訪問(wèn)前把這些回調(diào)也設(shè)置為無(wú)法訪問(wèn)。但涉及 setInterval 時(shí),下面這樣的代碼十分常見:

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)引用(更多信息見下文)。如今,大部分的瀏覽器都能而且會(huì)在對(duì)象變得不可到達(dá)的時(shí)候回收觀察處理器,即使監(jiān)聽器沒(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)聽器。這都是它們內(nèi)部處理,以保證不會(huì)產(chǎn)生內(nèi)存泄漏,甚至是在有問(wèn)題的瀏覽器(沒(méi)錯(cuò),IE6)上也不會(huì)。

3: 閉包

閉包是JavaScript開發(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樹中,另外一個(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樹內(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)用中,它就開始記錄所有的事情: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)在可以嘗試一下。

JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個(gè)常見的內(nèi)存泄露

參考資料

http://www-bcf.usc.edu/~dkempe/CS104/08-29.pdf https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156 http://www.nodesimplified.com/2017/08/javascript-memory-management-and.html Programming JavaScript Web Development Tutorial Memory Leak

來(lái)自:https://segmentfault.com/a/1190000011411121

標(biāo)簽: JavaScript
相關(guān)文章:
主站蜘蛛池模板: 土默特左旗| 嘉祥县| 景东| 察隅县| 读书| 三河市| 涞源县| 连云港市| 宜良县| 沐川县| 综艺| 新化县| 咸阳市| 津市市| 广河县| 固始县| 三穗县| 桃园县| 赣榆县| 诸城市| 尉氏县| 沐川县| 商城县| 黔西县| 广元市| 鄂伦春自治旗| 陆丰市| 开封市| 固阳县| 依安县| 涞源县| 托克逊县| 永吉县| 马边| 桑植县| 镇宁| 南漳县| 察哈| 孝感市| 永胜县| 太仆寺旗|