搜尋
首頁web前端js教程介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

Dec 09, 2020 pm 05:12 PM
javascript前端前端優化程式設計師

javascript欄位將討論另一個重要主題,記憶體管理

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

##相關免費學習推薦:javascript#(影片)

我們將討論另一個重要主題——記憶體管理,這是由於日常使用的程式語言越來越成熟和複雜,開發人員容易忽略這個問題。我們還將提供一些有關如何處理JavaScript中的記憶體洩漏的技巧,在SessionStack中遵循這些技巧,既能確保SessionStack 不會導致記憶體洩漏,也不會增加我們整合的Web應用程式的記憶體消耗。


想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!

概述

像 C 這樣的程式語言,具有低階記憶體管理原語,如malloc()和free()。開發人員使用這些原語明確地對作業系統的記憶體進行分配和釋放。

而JavaScript在創建物件(物件、字串等)時會為它們分配內存,不再使用對時會「自動」釋放內存,這個過程稱為垃圾收集。這種看「自動」似釋放資源的特性是造成混亂的根源,因為這給JavaScript(和其他高階語言)開發人員帶來一種錯覺,以為他們可以不關心記憶體管理的錯誤印象,這是想法一個大錯誤。

即使在使用高階語言時,開發人員也應該了解記憶體管理(或至少懂得一些基礎知識)。有時候,自動記憶體管理存在一些問題(例如垃圾收集器中的bug或實現限制等),開發人員必須理解這些問題,以便可以正確地處理它們(或找到一個適當的解決方案,以最小代價來維護代碼)。

記憶體的生命週期

無論使用哪一種程式語言,記憶體的生命週期都是一樣的:

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏##這裡簡單介紹一下記憶體生命週期中的每一個階段:

    分配記憶體
  • —  記憶體是由作業系統分配的,它允許您的程式使用它。在低階語言(例如C語言)中,這是一個開發人員需要自己處理的明確執行的操作。然而,在高階語言中,系統會自動為你分配內在。
  • 使用記憶體
  • — 這是程式實際使用先前分配的內存,在程式碼中使用分配的變數時,就會發生讀取和寫入操作。
  • 釋放記憶體
  • — 釋放所有不再使用的記憶體,使之成為自由記憶體,並且可以被重複使用。與分配記憶體操作一樣,這一操作在低階語言中也是需要明確地執行。
  • 記憶體是什麼?

在介紹JavaScript中的記憶體之前,我們將簡要討論記憶體是什麼以及它是如何運作的。

硬體層面上,電腦記憶體由大量的觸發器快取的。每個觸發器包含幾個晶體管,能夠儲存一位,單一觸發器都可以透過唯一標識符定址,因此我們可以讀取和覆寫它們。因此,從概念上講,可以把的整個電腦記憶體看作是一個可以讀寫的巨大數組。

身為人類,我們並不擅長用位元來思考和計算,所以我們把它們組織成更大的群組,這些群組一起可以用來表示數字。 8位元稱為1位元組。除了字節,還有字(有時是16位,有時是32位)。

很多東西都儲存在記憶體中:

程式使用的所有變數和其他資料。
  1. 程式的程式碼,包含作業系統的程式碼。
  2. 編譯器和作業系統一起為你處理大部分記憶體管理,但你還是需要了解底層的情況,對內在管理概念會有更深入的了解。

在編譯程式碼時,編譯器可以檢查基本資料類型,並提前計算它們需要多少記憶體。然後將所需的大小分配給呼叫堆疊空間中的程序,分配這些變數的空間稱為堆疊空間。因為當呼叫函數時,它們的記憶體將被添加到現有記憶體之上,當它們終止時,它們按照後進先出(LIFO)順序被移除。例如:

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏編譯器能夠立即知道所需的記憶體:4 4×4 8 = 28位元組。

這段程式碼展示了整數和雙精確度浮點型變數所佔記憶體的大小。但是大約20年前,整數變數通常佔2個位元組,而雙精確度浮點型變數佔4個位元組。你的程式碼不應該依賴目前基本資料類型的大小。

編譯器將插入與作業系統互動的程式碼,並申請儲存變數所需的堆疊位元組數。

在上面的例子中,編譯器知道每個變數的確切記憶體位址。事實上,每當我們寫入變數 n 時,它就會在內部被轉換成類似「記憶體位址4127963」這樣的資訊。

注意,如果我們嘗試存取 x[4],將存取與m關聯的資料。這是因為存取數組中一個不存在的元素(它比數組中最後一個實際分配的元素x[3]多4位元組),可能最終讀取(或覆蓋)一些 m 位元。這肯定會對程序的其餘部分產生不可預測的結果。

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

當函數呼叫其他函數時,每個函數在呼叫堆疊時獲得自己的區塊。它保存所有的局部變量,但也會有一個程式計數器來記住它在執行過程中的位置。當函數完成時,它的記憶體區塊將再次用於其他地方。

動態分配

不幸的是,當編譯時不知道一個變數需要多少記憶體時,事情就有點複雜了。假設我們想做如下的操作:

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

在編譯時,編譯器不知道陣列需要使用多少記憶體,因為這是由使用者提供的值決定的。

因此,它不能為堆疊上的變數分配空間。相反,我們的程式需要在運行時明確地向作業系統請求適當的空間,這個記憶體是從堆空間分配的。靜態記憶體分配與動態記憶體分配的差異總結如下表所示:

靜態記憶體分配 動態記憶體分配
#大小必須在編譯時知道 大小不需要在編譯時知道
#在編譯時執行 在執行時執行
分配給堆疊 分配給堆疊
#FILO (先進後出) #沒有特定的分配順序
#

要完全理解動態記憶體分配是如何運作的,需要在指標上花費更多的時間,這可能與本文的主題有太多的偏離,這裡就不太詳細介紹指標的相關的知識了。

在JavaScript中分配記憶體

現在將解釋第一步:如何在JavaScript中分配記憶體。

JavaScript為讓開發人員免於手動處理記憶體分配的責任-JavaScript自己進行記憶體分配同時宣告值。

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

某些函數呼叫也會導致物件的記憶體分配:

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

#方法可以分配新的值或對象:

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

在JavaScript中使用記憶體

在JavaScript中使用分配的記憶體意味著在其中讀寫,這可以透過讀取或寫入變量或物件屬性的值,或將參數傳遞給函數來實現。

當記憶體不再需要時進行釋放

大多數的記憶體管理問題都出現在這個階段

這裡最困難的地方是確定何時不再需要分配的內存,它通常要求開發人員確定程式中哪些地方不再需要內存的並釋放它。

高階語言嵌入了一種稱為垃圾收集器的機制,它的工作是追蹤記憶體分配和使用,以便發現任何時候一塊不再需要已分配的內在。在這種情況下,它將自動釋放這塊記憶體。

不幸的是,這個過程只​​是進行粗略估計,因為很難知道某塊內存是否真的需要 (不能通過算法來解決)。

大多數垃圾收集器透過收集不再被存取的記憶體來運作,例如,指向它的所有變數都超出了作用域。但是,這是可以收集的記憶體空間集合的一個不足估計值,因為在記憶體位置的任何一點上,仍然可能有一個變數在作用域中指向它,但是它將永遠不會再次存取。

垃圾收集

由於無法確定某些記憶體是否真的有用,因此,垃圾收集器想了一個辦法來解決這個問題。本節將解釋理解主要垃圾收集演算法及其限制。

記憶體引用

垃圾收集演算法主要依賴的是引用。

在記憶體管理上下文中,如果物件具有對另一個物件的存取權(可以是隱式的,也可以是顯式的),則稱物件引用另一個物件。例如,JavaScript物件具有對其原型(隱式參考)和屬性值(明確引用)的參考。

在此上下文中,「物件」的概念被擴展到比常規JavaScript物件更廣泛的範圍,並且還包含函數範圍(或全域詞法作用域)。

詞法作用域定義如何在巢狀函數中解析變數名:即使父函數已經傳回,內部函數也包含父函數的作用

引用計數垃圾收集演算法

#這是最簡單的垃圾收集演算法。如果沒有指向物件的引用,則認為該物件是「垃圾可回收的」,如下程式碼:

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

#循環會產生問題

當涉及到循環時,會有一個限制。在下面的範例中,創建了兩個物件,兩個物件互相引用,從而創建了一個循環。在函數呼叫之後將超出作用域,因此它們實際上是無用的,可以被釋放。然而,引用計數演算法認為,由於每個物件至少被引用一次,所以它們都不能被垃圾收集。

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

標記-清除(Mark-and-sweep)演算法

該演算法能夠判斷某個物件是否可以存取,從而知道該物件是否有用,該演算法由以下步驟組成:

  1. 垃圾收集器建立一個「根」列表,用於保存引用的全域變數。在JavaScript中,「window」物件是一個可作為根節點的全域變數。
  2. 然後,演算法檢查所有根及其子節點,並將它們標記為活動的(這意味著它們不是垃圾)。任何根不能到達的地方都會被標記為垃圾。
  3. 最後,垃圾收集器釋放所有未標記為活動的記憶體區塊,並將該記憶體傳回給作業系統。

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

這個演算法比上一個演算法要好,因為「一個物件沒有被引用」就意味著這個物件無法存取。

截至2012年,所有現代瀏覽器都有標記-清除垃圾收集器。過去幾年在JavaScript垃圾收集(分代/增量/並發/平行垃圾收集)領域所做的所有改進都是對該演算法(標記-清除)的實現改進,而不是對垃圾收集演算法本身的改進,也不是它決定物件是否可存取的目標。

在這篇文章中,你可以更詳細地閱讀到有關跟踪垃圾收集的詳細信息,同時還包括了標記-清除算法及其優化。

循環不再是問題

在上面的第一個例子中,在函數呼叫返回後,這兩個物件不再被從全域物件中可存取的物件引用。因此,垃圾收集器將發現它們無法存取。

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

儘管物件之間存在引用,但它們對於根節點來說是不可達的。

垃圾收集器的反直觀行為

儘管垃圾收集器很方便,但它們有一套自己的折衷方案,其中之一就是非決定論,換句話說,GC是不可預測的,你無法真正判斷何時進行垃圾收集。這意味著在某些情況下,程式會使用更多的記憶體,這實際上是必需的。在對速度特別敏感的應用程式中,可能會很明顯的感受到短時間的停頓。如果沒有分配記憶體,則大多數GC將處於空閒狀態。看看以下場景:

  1. 分配一組相當大的內在。
  2. 這些元素中的大多數(或全部)被標記為不可存取(假設引用指向一個不再需要的快取)。
  3. 不再進一步的分配

在這些場景中,大多數GCs 將不再繼續收集。換句話說,即使有不可訪問的引用可供收集,收集器也不會聲明這些引用。這些並不是嚴格意義上的洩漏,但仍然會導致比通常更高的記憶體使用。

記憶體洩漏是什麼?

從本質上說,記憶體洩漏可以定義為:不再被應用程式所需要的記憶體,出於某種原因,它不會回到操作系統或空閒記憶體池中。

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

程式語言支援不同的記憶體管理方式。然而,是否使用某一塊記憶體實際上是一個無法確定的問題。換句話說,只有開發人員才能明確指出一塊記憶體是否可以回到作業系統。

某些程式語言為開發人員提供了幫助,另一些則期望開發人員能清楚地了解記憶體何時不再被使用。維基百科上有一些有關人工和自動記憶體管理的很不錯的文章。

四種常見的記憶體洩漏

1.全域變數

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,或在完成後將其重新賦值。

2.被遺忘的計時器和回呼

setInterval為例,因為它在JavaScript中經常使用。

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每五秒会执行一次

上面的程式碼片段示範了使用計時器時引用不再需要的節點或資料。

renderer表示的物件可能會在未來的某個時間點被刪除,從而導致內部處理程序中的一整塊程式碼都變得不再需要。但是,由於定時器仍然是活動的,所以,處理程序不能被收集,並且其依賴項也無法被收集。這意味著,儲存大量資料的serverData也不能被收集。

在使用觀察者時,您需要確保在使用完它們之後進行明確調用來刪除它們(要么不再需要觀察者,要么物件將變得不可訪問)。

作為開發者時,需要確保在完成它們之後進行明確刪除它們(或物件將無法存取)。

在過去,有些瀏覽器無法處理這些情況(很好的IE6)。幸運的是,現在大多數現代瀏覽器會為幫你完成這項工作:一旦觀察到的物件變得不可訪問,即使忘記刪除偵聽器,它們也會自動收集觀察者處理程序。然而,我們還是應該在物件被處理之前明確地刪除這些觀察者。例如:

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

如今,現在的瀏覽器(包括IE和Edge)使用現代的垃圾回收演算法,可以立即發現並處理這些循環引用。換句話說,在一個節點刪除之前也不是必須要呼叫removeEventListener。

一些框架或函式庫,例如JQuery,會在處置節點之前自動刪除監聽器(在使用它們特定的API的時候)。這是由庫內部的機制實現的,能夠確保不發生內存洩漏,即使在有問題的瀏覽器下運行也能這樣,比如……IE 6。

3.閉包

閉包是javascript開發的關鍵方面,一個內部函數使用了外部(封閉)函數的變數。由於JavaScript運行的細節,它可能以下面的方式造成記憶體洩漏:

介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏

#這段程式碼做了一件事:每次都呼叫replaceThing#的時候,theThing都會得到一個包含一個大數組和一個新閉包(someMethod)的新物件。同時,變數unused指向一個引用了`originalThing的閉包。

是不是有點困惑了? 重要的是,一旦具有相同父作用域的多個閉包的作用域被創建,則這個作用域就可以被共享。

在這種情況下,為閉包someMethod而建立的作用域可以被unused共享的。 unused內部存在一個對originalThing的引用。即使unused從未使用過,someMethod也可以在replaceThing的作用域之外(例如在全域範圍內)透過theThing來被調用。

由於someMethod共享了unused閉包的作用域,那麼unused引用包含的originalThing會迫使它保持活動狀態(兩個閉包之間的整個共享作用域)。這阻止了它被收集。

當這段程式碼重複運行時,可以觀察到記憶體使用在穩定增長,當GC運行後,記憶體使用也不會變小。從本質上說,在運行過程中創建了一個閉包鍊錶(它的根是以變量theThing的形式存在),並且每個閉包的作用域都間接引用了一個大數組,這造成了相當大的內存洩漏。

4.脫離DOM的引用

有時,將DOM節點儲存在資料結構中可能會很有用。假設你希望快速地更新表中的幾行內容,那麼你可以在一個字典或數組中保存每個DOM行的引用。這樣,同一個DOM元素就存在兩個引用:一個在DOM樹中,另一個則在字典中。如果在將來的某個時候你決定刪除這些行,那麼你需要將這兩個引用都設為不可訪問。

在引用 DOM 樹中的內部節點或葉節點時,還需要考慮另一個問題。如果在程式碼中保留對錶單元格的引用(

標記),並決定從 DOM 中刪除表,同時保留對該特定單元格的引用,那麼可能會出現記憶體洩漏。

你可能認為垃圾收集器將釋放除該單元格之外的所有內容。然而,事實並非如此,由於單元格是表的一個子節點,而子節點保存對父節點的引用,所以對錶單元格的這個引用將使整個表保持在內存中,所以在移除有被引用的節點時候要移除其子節點。

以上是介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:segmentfault。如有侵權,請聯絡admin@php.cn刪除
Python vs. JavaScript:學習曲線和易用性Python vs. JavaScript:學習曲線和易用性Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

Python vs. JavaScript:社區,圖書館和資源Python vs. JavaScript:社區,圖書館和資源Apr 15, 2025 am 12:16 AM

Python和JavaScript在社區、庫和資源方面的對比各有優劣。 1)Python社區友好,適合初學者,但前端開發資源不如JavaScript豐富。 2)Python在數據科學和機器學習庫方面強大,JavaScript則在前端開發庫和框架上更勝一籌。 3)兩者的學習資源都豐富,但Python適合從官方文檔開始,JavaScript則以MDNWebDocs為佳。選擇應基於項目需求和個人興趣。

從C/C到JavaScript:所有工作方式從C/C到JavaScript:所有工作方式Apr 14, 2025 am 12:05 AM

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

JavaScript引擎:比較實施JavaScript引擎:比較實施Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

超越瀏覽器:現實世界中的JavaScript超越瀏覽器:現實世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

使用Next.js(後端集成)構建多租戶SaaS應用程序使用Next.js(後端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

如何使用Next.js(前端集成)構建多租戶SaaS應用程序如何使用Next.js(前端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:22 AM

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript:探索網絡語言的多功能性JavaScript:探索網絡語言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
4 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具