摘要:分配這些變量的空間稱為堆棧空間,因為隨著函數(shù)的調用,它們的內存將被添加到現(xiàn)有內存之上。當函數(shù)調用其他函數(shù)時,每個函數(shù)在調用時都會獲得自己的堆棧塊。
該系列的第一篇文章重點介紹了引擎,運行時和調用堆棧的概述。第二篇文章深入剖析了Google的V8 JavaScript引擎,并提供了關于如何編寫更好的JavaScript代碼的一些提示。
在第三篇文章中,我們將討論另一個越來越被開發(fā)人員忽視的關鍵主題,因為日常使用的編程語言(內存管理)越來越成熟和復雜。我們還會提供一些關于如何處理內存泄漏的技巧。
概述像C這樣的編程語言,提供從底層上管理內存的方法,如malloc()和free()。開發(fā)人員使用這些方法,用來從操作系統(tǒng)分配內存,或釋放內存到操作系統(tǒng)中。
當對象或字符串等被創(chuàng)建時,JavaScript會申請和分配內存;當對象或字符不在被使用時,它們就會被自動釋放,這也被稱為垃圾處理。這種釋放資源的看似是“自動”的,這恰恰是誤解的來源,給JavaScript(以及其他高級語言)開發(fā)人員造成了他們可能選擇不關心內存管理的錯誤印象。這是一個大錯誤。
即使使用高級語言,開發(fā)人員也應該理解內存管理。有時自動內存管理也會出現(xiàn)問題(如bugs或者垃圾回收限制等),開發(fā)人員不得不先了解它們,然后才能妥善處理。
內存生命周期無論您使用什么編程語言,內存生命周期幾乎都是一樣的:
以下簡單描述了在該周期的每個步驟中發(fā)生的情況:
分配內存 - 內存由操作系統(tǒng)分配,允許程序使用它。在底層語言(如C)中,這是一個顯式操作,您作為開發(fā)人員應該處理。然而,在高級語言中,這個操作被隱藏了。
使用內存 - 這是您的程序實際使用之前分配的內存。讀取和寫入操作發(fā)生在您在代碼中使用分配的變量時。
釋放內存 - 現(xiàn)在是釋放您不需要的整個內存的時間,以便它可以變?yōu)榭臻e并再次可用。 與分配內存操作一樣,這個操作在底層語言中是可以直接調用的。
有關調用堆棧和內存堆的概念的概述,您可以閱讀本系列第一篇文章。
什么是內存?在開始討論JavaScript的內存之前,我們將簡要討論一般內存概念以及它如何工作。
在硬件級別上,計算機內存由大量的觸發(fā)器。每個觸發(fā)器都包含一些晶體管并且能夠存儲一個bit。單個觸發(fā)器可通過唯一標識符進行尋址,因此我們可以讀取并覆蓋它們。因此,從概念上講,我們可以將整個計算機內存看作是我們可以讀寫的bit數(shù)組。
從人類角度來說,我們不擅長用bit來完成我們現(xiàn)實中思想和算法,我們把它們組織成更大的部分,它們一起可以用來表示數(shù)字。 8位(比特位)稱為1個字節(jié)(byte)。除字節(jié)外,還有單詞(word)(有時是16,有時是32位)。
很多東西都存儲在這個內存中:
所有程序使用的所有變量和其他數(shù)據(jù)。
程序的代碼,包括操作系統(tǒng)的代碼。
編譯器和操作系統(tǒng)一起工作,為您處理大部分內存管理,但我們建議您看看底下發(fā)生了什么。
編譯代碼時,編譯器可以檢查原始數(shù)據(jù)類型并提前計算它們需要多少內存。然后將所需的內存分配給調用堆棧空間中的程序。分配這些變量的空間稱為堆棧空間,因為隨著函數(shù)的調用,它們的內存將被添加到現(xiàn)有內存之上。當它們終止時,它們以LIFO(后進先出)順序被移除。例如,請考慮以下聲明:
int n; // 4字節(jié) int x [4]; // 4個元素的數(shù)組,每個4個字節(jié) double m; // 8個字節(jié)
編譯器可以立即看到代碼需要
4 + 4×4 + 8 = 28個字節(jié)。
這就是它如何處理整數(shù)和雙精度的當前大小。大約20年前,整數(shù)通常是2個字節(jié),并且是雙4字節(jié)。您的代碼不應該依賴于此時基本數(shù)據(jù)類型的大小。
編譯器將插入與操作系統(tǒng)進行交互的代碼,以在堆棧中請求必要的字節(jié)數(shù),以便存儲變量。
在上面的例子中,編譯器知道每個變量的確切內存地址。事實上,只要我們寫入變量n,就會在內部翻譯成類似“內存地址4127963”的內容。
注意,如果我們試圖在這里訪問x[4],我們將訪問與m關聯(lián)的數(shù)據(jù)。這是因為我們正在訪問數(shù)組中不存在的元素 - 它比數(shù)組中最后一個實際分配的元素x [3]更遠了4個字節(jié),并且可能最終讀取(或覆蓋)m個位中的一些位。這對方案的其余部分幾乎肯定會有非常不希望的后果。
當函數(shù)調用其他函數(shù)時,每個函數(shù)在調用時都會獲得自己的堆棧塊。它保留了它所有的局部變量,同時還有一個程序計數(shù)器,記錄它在執(zhí)行時的位置。當功能完成時,其存儲器塊再次可用于其他目的。
動態(tài)分配內存不幸的是,當我們在編譯時有時不知道變量需要多少內存時,假設我們想要做如下的事情:
int n = readInput(); //用戶輸入 ... //常見一個長度為n的數(shù)組
在編譯時,編譯器不知道數(shù)組需要多少內存,因為它由用戶提供的值決定。
因此,它不能為堆棧上的變量分配空間。 相反,我們的程序需要在運行時明確要求操作系統(tǒng)提供適當?shù)目臻g。 該內存是從堆空間分配的。 下表總結了靜態(tài)和動態(tài)內存分配之間的區(qū)別:
為了充分理解動態(tài)內存分配是如何工作的,我們需要在指針上花費更多時間,這可能與本文的主題偏離太多。 如果您有興趣了解更多信息,請在評論中告訴我們,我們可以在以后的文章中詳細介紹指針。
JavaScript分配內存現(xiàn)在我們將解釋第一步(分配內存),以及它如何在JavaScript中工作。
JavaScript減輕了開發(fā)人員處理內存分配的責任 - JavaScript自身聲明的時候就分配內存,然后賦值。
var n = 374; // 為數(shù)字分配內存 var s = "sessionstack"; // 為字符串分配內存 var o = { a: 1, b: null }; // 為對象和它的值分配內存 var a = [1, null, "str"]; // (類似對象) 為數(shù)組和它的值 // 分配內存 function f(a) { return a + 3; } // 為函數(shù)分配內存 (which is a callable object) // 函數(shù)表達式也會分配內存 someElement.addEventListener("click", function() { someElement.style.backgroundColor = "blue"; }, false);
一些函數(shù)調用也會導致對象分配:
var d = new Date(); // 為日期對象分配內存 var e = document.createElement("div"); // 為DOM元素分配內存
方法可以分配新的值或對象:
var s1 = "sessionstack"; var s2 = s1.substr(0, 3); // s2 is a new string // 由于字符串是不可改變的, // JavaScript may decide to not allocate memory, // but just store the [0, 3] range. var a1 = ["str1", "str2"]; var a2 = ["str3", "str4"]; var a3 = a1.concat(a2); // new array with 4 elements being // the concatenation of a1 and a2 elements在JavaScript中使用內存
基本上在JavaScript中使用分配的內存意味著讀取和寫入。
這可以通過讀取或寫入變量或對象屬性的值,或者甚至將參數(shù)傳遞給函數(shù)來完成。
當內存不再需要時釋放大部分內存管理問題都是在這個階段出現(xiàn)的。
這里最困難的任務是確定何時不再需要分配的內存。它通常需要開發(fā)人員確定程序中的哪個地方不再需要這些內存,并將其釋放。
高級語言嵌入了一個名為垃圾收集器的軟件,其工作是跟蹤內存分配和使用情況,以便找到何時不再需要分配的內存,在這種情況下,它會自動釋放它。
不幸的是,這個過程是一個大概,因為知道是否需要某些內存的一般問題是不可判定的(不能由算法解決)。
大多數(shù)垃圾收集器通過收集不能再訪問的內存來工作,例如,指向它的所有變量都超出了范圍。然而,這是可以收集的一組內存空間的近似值,因為在任何時候內存位置可能仍然有一個指向它的變量,但它將不會再被訪問。
垃圾收集
由于發(fā)現(xiàn)某些內存是否“不再需要”的事實是不可判定的,所以垃圾收集實現(xiàn)了對一般問題的解決方案的限制。本節(jié)將解釋理解主要垃圾收集算法及其局限性的必要概念。
垃圾收集算法所依賴的主要概念是參考之一。
在內存管理的上下文中,如果一個對象可以訪問后者(可以是隱式或顯式的),則稱該對象引用另一個對象。例如,JavaScript對象具有對其原型(隱式引用)及其屬性值(顯式引用)的引用。
在這種情況下,“對象”的概念擴展到比常規(guī)JavaScript對象更廣泛的范圍,并且還包含函數(shù)范圍(或全局詞法范圍)。
詞法范圍定義了如何在嵌套函數(shù)中解析變量名稱:即使父函數(shù)已返回,內部函數(shù)也包含父函數(shù)的作用域。
4種常見的內存泄漏 1. 全局變量JavaScript以一種有趣的方式處理未聲明的變量:當引用未聲明的變量時,會在全局對象中創(chuàng)建一個新變量。 在瀏覽器中,全局對象將是window,這意味著
function foo(arg) { bar = "some text"; }
等同于
function foo(arg) { window.bar = "some text"; }
假設bar的目的是僅引用foo函數(shù)中的變量。但是,如果您不使用var來聲明它,將會創(chuàng)建一個冗余的全局變量。在上述情況下,這不會造成太大的傷害。 盡管如此,你一定可以想象一個更具破壞性的場景。
你也可以用這個意外地創(chuàng)建一個全局變量:
function foo() { this.var1 = "potential accidental global"; } // Foo called on its own, this points to the global object (window) // rather than being undefined. foo();
您可以通過添加"use strict"來避免這些問題; 在您的JavaScript文件的開始處,它將切換更嚴格的解析JavaScript模式,從而防止意外創(chuàng)建全局變量。
意外的全局變量當然是一個問題,然而,更多的時候,你的代碼會受到顯式定義的全局變量的影響,這些變量不能被垃圾收集器回收。需要特別注意用于臨時存儲和處理大量信息的全局變量。如果你必須使用全局變量來存儲數(shù)據(jù),用完之后一定要把它賦值為null或者在完成之后重新賦值。
2. 被遺忘的定時器和回調函數(shù)以setInterval為例,因為它經常在JavaScript中使用。
提供觀察者模式或接受回調的工具庫,它通常會確保當其實例無法訪問時,其所回調的引用在變得無法訪問。下面的代碼并不罕見:
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é)點或數(shù)據(jù)的定時器的后果。
renderer對象可能會被替換或刪除,這會使得間隔處理程序封裝的塊變得冗余。如果發(fā)生這種情況,則不需要收集處理程序及其依賴關系,因為interval需要先停止(請記住,它仍然處于活動狀態(tài))。這一切歸結為serverData確實存儲和處理負載數(shù)據(jù)的事實也不會被收集。
當使用observers時,你需要確保你做了一個明確的調用,在完成它們之后將其刪除(不再需要觀察者,否則對象將無法訪問)。
幸運的是,大多數(shù)現(xiàn)代瀏覽器都會為您完成這項工作:即使您忘記刪除偵聽器,一旦觀察到的對象變得無法訪問,他們會自動收集觀察者處理程序。在過去,一些瀏覽器無法處理這些情況(舊版IE6)。
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)在的瀏覽器支持可以檢測這些周期并適當處理它們的垃圾收集器,因此在使節(jié)點無法訪問之前,不再需要調用removeEventListener。
如果您利用jQuery API(其他庫和框架也支持這一點),您也可以在節(jié)點過時之前刪除偵聽器。 即使應用程序在較舊的瀏覽器版本下運行,該庫也會確保沒有內存泄漏。
3. 閉包JavaScript開發(fā)的一個關鍵點是閉包:一個可以訪問外部函數(shù)的變量的內部函數(shù)。由于JavaScript運行時的實現(xiàn)方式,可能以下列方式泄漏內存:
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函數(shù)被調用,theThing變量將被賦值為一個由很長的字符串和一個新閉包(someMethod)組成的新對象。originalThing變量被一個閉包引用,這個閉包由unused變量保持。需要記住的是,當一個閉包的作用域被創(chuàng)建,同屬父范圍內的閉包的作用域會被共享。
在這種情況下,閉包someMethod創(chuàng)建的作用域將與閉包unused的作用域共享。unused引用了originalThing,盡管代碼中unused從未被調用過,但是我們還是可以在replaceThing函數(shù)外通過theThing來調用someMethod。由于someMethod與unused的閉包作用域共享,閉包unused的引用了originalThing,強制它保持活動狀態(tài)(兩個閉包之間的共享作用域)。這阻止了它被垃圾回收。
在上面的例子中,閉包someMethod創(chuàng)建的作用域與閉包unused作用域的共享,而unused的引用originalThing。盡管閉包unused從未被使用,someMethod還是可以通過theThing,從replaceThing范圍外被調用。事實上,閉包unused引用了originalThing要求它保持活動,因為someMethod與unused的作用域共享。
閉包會保留一個指向其作用域的指針,作用域就是閉包父函數(shù),所以閉包unused和someMethod都會有一個指針指向replaceThing函數(shù),這也是為什么閉包可以訪問外部函數(shù)的變量。由于閉包unused引用了originalThing變量,這使得originalThing變量存在于lexical environment,replaceThing函數(shù)里面定義的所有的閉包都會有一個對originalThing的引用,所以閉包someMethod自然會保持一個對originalThing的引用,所以就算theThing替換成其它值,它的上一次值不會被回收。
所有這些都可能導致相當大的內存泄漏。當上面的代碼片段一遍又一遍地運行時,您可能會發(fā)現(xiàn)內存使用量激增。當垃圾收集器運行時,其大小不會縮小。創(chuàng)建了一個閉包的鏈表(在這種情況下,它的根就是theThing變量),并且每個閉包范圍都會間接引用大數(shù)組。
4. DOM樹之外的引用有些情況下開發(fā)者會保存DOM節(jié)點的引用。假設你想快速更新表格中幾行的內容,如果使用字典或數(shù)組存儲這幾行的DOM引用,則會有兩個對同一DOM元素的引用:一個在DOM樹中,另一個在字典或數(shù)組中。如果你決定刪除并回收這些行,您需要記住要使這個兩個引用都無法訪問。
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")); // 這時我們還有一個對 #image 的引用,這個引用在elements對象中 // 換句話說,image元素還在內存中,不能被GC回收 }
涉及DOM樹內的內部節(jié)點或葉節(jié)點時,還有一個額外需要考慮的問題。如果在代碼中保留對表格單元格(
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/94668.html
摘要:這是因為我們訪問了數(shù)組中不存在的數(shù)組元素它超過了最后一個實際分配到內存的數(shù)組元素字節(jié),并且有可能會讀取或者覆寫的位。包含個元素的新數(shù)組由和數(shù)組元素所組成中的內存使用中使用分配的內存主要指的是內存讀寫。 原文請查閱這里,本文有進行刪減,文后增了些經驗總結。 本系列持續(xù)更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第三章。 我們將會討論日常使用中另一個被開發(fā)...
摘要:是如何工作的內存管理以及如何處理四種常見的內存泄漏原文譯者幾個禮拜之前我們開始一系列對于以及其本質工作原理的深入挖掘我們認為通過了解的構建方式以及它們是如何共同合作的,你就能夠寫出更好的代碼以及應用。 JavaScript是如何工作的:內存管理以及如何處理四種常見的內存泄漏 原文:How JavaScript works: memory management + how to han...
摘要:本系列的第一篇文章簡單介紹了引擎運行時間和堆棧的調用。編譯器將插入與操作系統(tǒng)交互的代碼,并申請存儲變量所需的堆棧字節(jié)數(shù)。當函數(shù)調用其他函數(shù)時,每個函數(shù)在調用堆棧時獲得自己的塊。因此,它不能為堆棧上的變量分配空間。 本系列的第一篇文章簡單介紹了引擎、運行時間和堆棧的調用。第二篇文章研究了谷歌V8 JavaScript引擎的內部機制,并介紹了一些編寫JavaScript代碼的技巧。 在這第...
摘要:解決方式是,當我們不使用它們的時候,手動切斷鏈接淘汰把和對象轉為了真正的對象,避免了使用這種垃圾收集策略,消除了以下常見的內存泄漏的主要原因。以上參考資料高程垃圾收集類內存泄漏及如何避免內存泄露及解決方案詳解類內存泄漏及如何避免 showImg(http://ww1.sinaimg.cn/large/005Y4rCogy1ft1ikzcqzqj30ka0et77a.jpg); 前言 起...
閱讀 1414·2021-11-22 09:34
閱讀 1384·2021-09-22 14:57
閱讀 3417·2021-09-10 10:50
閱讀 1398·2019-08-30 15:54
閱讀 3697·2019-08-29 17:02
閱讀 3479·2019-08-29 12:54
閱讀 2621·2019-08-27 10:57
閱讀 3325·2019-08-26 12:24