您好,登錄后才能下訂單哦!
這篇文章主要介紹了JavaScript如何處理內存泄漏,具有一定借鑒價值,需要的朋友可以參考下。希望大家閱讀完這篇文章后大有收獲。下面讓小編帶著大家一起了解一下。
像 C 這樣的編程語言,具有低級內存管理原語,如malloc()和free()。開發人員使用這些原語顯式地對操作系統的內存進行分配和釋放。
而JavaScript在創建對象(對象、字符串等)時會為它們分配內存,不再使用對時會“自動”釋放內存,這個過程稱為垃圾收集。這種看“自動”似釋放資源的的特性是造成混亂的根源,因為這給JavaScript(和其他高級語言)開發人員帶來一種錯覺,以為他們可以不關心內存管理的錯誤印象,這是想法一個大錯誤。
即使在使用高級語言時,開發人員也應該了解內存管理(或者至少懂得一些基礎知識)。有時候,自動內存管理存在一些問題(例如垃圾收集器中的bug或實現限制等),開發人員必須理解這些問題,以便可以正確地處理它們(或者找到一個適當的解決方案,以最小代價來維護代碼)。
無論使用哪種編程語言,內存的生命周期都是一樣的:
這里簡單介紹一下內存生命周期中的每一個階段:
在介紹JavaScript中的內存之前,我們將簡要討論內存是什么以及它是如何工作的。
硬件層面上,計算機內存由大量的觸發器緩存的。每個觸發器包含幾個晶體管,能夠存儲一位,單個觸發器都可以通過唯一標識符尋址,因此我們可以讀取和覆蓋它們。因此,從概念上講,可以把的整個計算機內存看作是一個可以讀寫的巨大數組。
作為人類,我們并不擅長用比特來思考和計算,所以我們把它們組織成更大的組,這些組一起可以用來表示數字。8位稱為1字節。除了字節,還有字(有時是16位,有時是32位)。
很多東西都存儲在內存中:
編譯器和操作系統一起為你處理大部分內存管理,但是你還是需要了解一下底層的情況,對內在管理概念會有更深入的了解。
在編譯代碼時,編譯器可以檢查基本數據類型,并提前計算它們需要多少內存。然后將所需的大小分配給調用堆棧空間中的程序,分配這些變量的空間稱為堆棧空間。因為當調用函數時,它們的內存將被添加到現有內存之上,當它們終止時,它們按照后進先出(LIFO)順序被移除。例如:
編譯器能夠立即知道所需的內存:4 + 4×4 + 8 = 28字節。
這段代碼展示了整型和雙精度浮點型變量所占內存的大小。但是大約20年前,整型變量通常占2個字節,而雙精度浮點型變量占4個字節。你的代碼不應該依賴于當前基本數據類型的大小。
編譯器將插入與操作系統交互的代碼,并申請存儲變量所需的堆棧字節數。
在上面的例子中,編譯器知道每個變量的確切內存地址。事實上,每當我們寫入變量 n
時,它就會在內部被轉換成類似“內存地址4127963”
這樣的信息。
注意,如果我們嘗試訪問 x[4]
,將訪問與m關聯的數據。這是因為訪問數組中一個不存在的元素(它比數組中最后一個實際分配的元素x[3]多4字節),可能最終讀取(或覆蓋)一些 m 位。這肯定會對程序的其余部分產生不可預知的結果。
當函數調用其他函數時,每個函數在調用堆棧時獲得自己的塊。它保存所有的局部變量,但也會有一個程序計數器來記住它在執行過程中的位置。當函數完成時,它的內存塊將再次用于其他地方。
不幸的是,當編譯時不知道一個變量需要多少內存時,事情就有點復雜了。假設我們想做如下的操作:
在編譯時,編譯器不知道數組需要使用多少內存,因為這是由用戶提供的值決定的。
因此,它不能為堆棧上的變量分配空間。相反,我們的程序需要在運行時顯式地向操作系統請求適當的空間,這個內存是從堆空間分配的。靜態內存分配和動態內存分配的區別總結如下表所示:
靜態內存分配 | 動態內存分配 |
---|---|
大小必須在編譯時知道 | 大小不需要在編譯時知道 |
在編譯時執行 | 在運行時執行 |
分配給堆棧 | 分配給堆 |
FILO (先進后出) | 沒有特定的分配順序 |
要完全理解動態內存分配是如何工作的,需要在指針上花費更多的時間,這可能與本文的主題有太多的偏離,這里就不太詳細介紹指針的相關的知識了。
現在將解釋第一步:如何在JavaScript中分配內存。
JavaScript為讓開發人員免于手動處理內存分配的責任——JavaScript自己進行內存分配同時聲明值。
某些函數調用也會導致對象的內存分配:
方法可以分配新的值或對象:
在JavaScript中使用分配的內存意味著在其中讀寫,這可以通過讀取或寫入變量或對象屬性的值,或者將參數傳遞給函數來實現。
大多數的內存管理問題都出現在這個階段
這里最困難的地方是確定何時不再需要分配的內存,它通常要求開發人員確定程序中哪些地方不再需要內存的并釋放它。
高級語言嵌入了一種稱為垃圾收集器的機制,它的工作是跟蹤內存分配和使用,以便發現任何時候一塊不再需要已分配的內在。在這種情況下,它將自動釋放這塊內存。
不幸的是,這個過程只是進行粗略估計,因為很難知道某塊內存是否真的需要 (不能通過算法來解決)。
大多數垃圾收集器通過收集不再被訪問的內存來工作,例如,指向它的所有變量都超出了作用域。但是,這是可以收集的內存空間集合的一個不足估計值,因為在內存位置的任何一點上,仍然可能有一個變量在作用域中指向它,但是它將永遠不會被再次訪問。
由于無法確定某些內存是否真的有用,因此,垃圾收集器想了一個辦法來解決這個問題。本節將解釋理解主要垃圾收集算法及其局限性。
垃圾收集算法主要依賴的是引用。
在內存管理上下文中,如果對象具有對另一個對象的訪問權(可以是隱式的,也可以是顯式的),則稱對象引用另一個對象。例如,JavaScript對象具有對其原型(隱式引用)和屬性值(顯式引用)的引用。
在此上下文中,“對象”的概念被擴展到比常規JavaScript對象更廣泛的范圍,并且還包含函數范圍(或全局詞法作用域)。
詞法作用域定義了如何在嵌套函數中解析變量名:即使父函數已經返回,內部函數也包含父函數的作用
這是最簡單的垃圾收集算法。如果沒有指向對象的引用,則認為該對象是“垃圾可回收的”,如下代碼:
當涉及到循環時,會有一個限制。在下面的示例中,創建了兩個對象,兩個對象互相引用,從而創建了一個循環。在函數調用之后將超出作用域,因此它們實際上是無用的,可以被釋放。然而,引用計數算法認為,由于每個對象至少被引用一次,所以它們都不能被垃圾收集。
該算法能夠判斷出某個對象是否可以訪問,從而知道該對象是否有用,該算法由以下步驟組成:
這個算法比上一個算法要好,因為“一個對象沒有被引用”就意味著這個對象無法訪問。
截至2012年,所有現代瀏覽器都有標記-清除垃圾收集器。過去幾年在JavaScript垃圾收集(分代/增量/并發/并行垃圾收集)領域所做的所有改進都是對該算法(標記-清除)的實現改進,而不是對垃圾收集算法本身的改進,也不是它決定對象是否可訪問的目標。
在這篇文章中,你可以更詳細地閱讀到有關跟蹤垃圾收集的詳細信息,同時還包括了標記-清除算法及其優化。
在上面的第一個例子中,在函數調用返回后,這兩個對象不再被從全局對象中可訪問的對象引用。因此,垃圾收集器將發現它們不可訪問。
盡管對象之間存在引用,但它們對于根節點來說是不可達的。
盡管垃圾收集器很方便,但它們有一套自己的折衷方案,其中之一就是非決定論,換句話說,GC是不可預測的,你無法真正判斷何時進行垃圾收集。這意味著在某些情況下,程序會使用更多的內存,這實際上是必需的。在對速度特別敏感的應用程序中,可能會很明顯的感受到短時間的停頓。如果沒有分配內存,則大多數GC將處于空閑狀態。看看以下場景:
在這些場景中,大多數GCs 將不再繼續收集。換句話說,即使有不可訪問的引用可供收集,收集器也不會聲明這些引用。這些并不是嚴格意義上的泄漏,但仍然會導致比通常更高的內存使用。
從本質上說,內存泄漏可以定義為:不再被應用程序所需要的內存,出于某種原因,它不會返回到操作系統或空閑內存池中。
編程語言支持不同的內存管理方式。然而,是否使用某一塊內存實際上是一個無法確定的問題。換句話說,只有開發人員才能明確一塊內存是否可以返回到操作系統。
某些編程語言為開發人員提供了幫助,另一些則期望開發人員能清楚地了解內存何時不再被使用。維基百科上有一些有關人工和自動內存管理的很不錯的文章。
JavaScript以一種有趣的方式處理未聲明的變量: 對于未聲明的變量,會在全局范圍中創建一個新的變量來對其進行引用。在瀏覽器中,全局對象是window。例如:
function foo(arg) { bar = "some text"; }
等價于:
function foo(arg) { window.bar = "some text"; }
如果bar在foo函數的作用域內對一個變量進行引用,卻忘記使用var來聲明它,那么將創建一個意想不到的全局變量。在這個例子中,遺漏一個簡單的字符串不會造成太大的危害,但這肯定會很糟。
創建一個意料之外的全局變量的另一種方法是使用this:
function foo() { this.var1 = "potential accidental global"; } // Foo自己調用,它指向全局對象(window),而不是未定義。 foo();
可以在JavaScript文件的開頭通過添加“use strict”來避免這一切,它將開啟一個更嚴格的JavaScript解析模式,以防止意外創建全局變量。
盡管我們討論的是未知的全局變量,但仍然有很多代碼充斥著顯式的全局變量。根據定義,這些是不可收集的(除非被指定為空或重新分配)。用于臨時存儲和處理大量信息的全局變量特別令人擔憂。如果你必須使用一個全局變量來存儲大量數據,那么請確保將其指定為null,或者在完成后將其重新賦值。
以setInterval
為例,因為它在JavaScript中經常使用。
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //每五秒會執行一次
上面的代碼片段演示了使用定時器時引用不再需要的節點或數據。
renderer表示的對象可能會在未來的某個時間點被刪除,從而導致內部處理程序中的一整塊代碼都變得不再需要。但是,由于定時器仍然是活動的,所以,處理程序不能被收集,并且其依賴項也無法被收集。這意味著,存儲著大量數據的serverData也不能被收集。
在使用觀察者時,您需要確保在使用完它們之后進行顯式調用來刪除它們(要么不再需要觀察者,要么對象將變得不可訪問)。
作為開發者時,需要確保在完成它們之后進行顯式刪除它們(或者對象將無法訪問)。
在過去,一些瀏覽器無法處理這些情況(很好的IE6)。幸運的是,現在大多數現代瀏覽器會為幫你完成這項工作:一旦觀察到的對象變得不可訪問,即使忘記刪除偵聽器,它們也會自動收集觀察者處理程序。然而,我們還是應該在對象被處理之前顯式地刪除這些觀察者。例如:
如今,現在的瀏覽器(包括IE和Edge)使用現代的垃圾回收算法,可以立即發現并處理這些循環引用。換句話說,在一個節點刪除之前也不是必須要調用removeEventListener。
一些框架或庫,比如JQuery,會在處置節點之前自動刪除監聽器(在使用它們特定的API的時候)。這是由庫內部的機制實現的,能夠確保不發生內存泄漏,即使在有問題的瀏覽器下運行也能這樣,比如……IE 6。
閉包是javascript開發的一個關鍵方面,一個內部函數使用了外部(封閉)函數的變量。由于JavaScript運行的細節,它可能以下面的方式造成內存泄漏:
這段代碼做了一件事:每次調用replaceThing
的時候,theThing
都會得到一個包含一個大數組和一個新閉包(someMethod)的新對象。同時,變量unuse
d指向一個引用了`originalThing
的閉包。
是不是有點困惑了? 重要的是,一旦具有相同父作用域的多個閉包的作用域被創建,則這個作用域就可以被共享。
在這種情況下,為閉包someMethod
而創建的作用域可以被unused
共享的。unused
內部存在一個對originalThing
的引用。即使unused
從未使用過,someMethod
也可以在replaceThing
的作用域之外(例如在全局范圍內)通過theThing
來被調用。
由于someMethod
共享了unused
閉包的作用域,那么unused
引用包含的originalThing
會迫使它保持活動狀態(兩個閉包之間的整個共享作用域)。這阻止了它被收集。
當這段代碼重復運行時,可以觀察到內存使用在穩定增長,當GC
運行后,內存使用也不會變小。從本質上說,在運行過程中創建了一個閉包鏈表(它的根是以變量theThing
的形式存在),并且每個閉包的作用域都間接引用了了一個大數組,這造成了相當大的內存泄漏。
有時,將DOM節點存儲在數據結構中可能會很有用。假設你希望快速地更新表中的幾行內容,那么你可以在一個字典或數組中保存每個DOM行的引用。這樣,同一個DOM元素就存在兩個引用:一個在DOM樹中,另一個則在字典中。如果在將來的某個時候你決定刪除這些行,那么你需要將這兩個引用都設置為不可訪問。
在引用 DOM 樹中的內部節點或葉節點時,還需要考慮另外一個問題。如果在代碼中保留對表單元格的引用(<td>標記),并決定從 DOM 中刪除表,同時保留對該特定單元格的引用,那么可能會出現內存泄漏。
你可能認為垃圾收集器將釋放除該單元格之外的所有內容。然而,事實并非如此,由于單元格是表的一個子節點,而子節點保存對父節點的引用,所以對表單元格的這個引用將使整個表保持在內存中,所以在移除有被引用的節點時候要移除其子節點。
感謝你能夠認真閱讀完這篇文章,希望小編分享JavaScript如何處理內存泄漏內容對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,遇到問題就找億速云,詳細的解決方法等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。