介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏
javascript欄位將討論另一個重要主題,記憶體管理
我們將討論另一個重要主題——記憶體管理,這是由於日常使用的程式語言越來越成熟和複雜,開發人員容易忽略這個問題。我們還將提供一些有關如何處理JavaScript中的記憶體洩漏的技巧,在SessionStack中遵循這些技巧,既能確保SessionStack 不會導致記憶體洩漏,也不會增加我們整合的Web應用程式的記憶體消耗。##相關免費學習推薦:javascript#(影片)
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
概述像 C 這樣的程式語言,具有低階記憶體管理原語,如malloc()和free()。開發人員使用這些原語明確地對作業系統的記憶體進行分配和釋放。 而JavaScript在創建物件(物件、字串等)時會為它們分配內存,不再使用對時會「自動」釋放內存,這個過程稱為垃圾收集。這種看「自動」似釋放資源的特性是造成混亂的根源,因為這給JavaScript(和其他高階語言)開發人員帶來一種錯覺,以為他們可以不關心記憶體管理的錯誤印象,這是想法一個大錯誤。 即使在使用高階語言時,開發人員也應該了解記憶體管理(或至少懂得一些基礎知識)。有時候,自動記憶體管理存在一些問題(例如垃圾收集器中的bug或實現限制等),開發人員必須理解這些問題,以便可以正確地處理它們(或找到一個適當的解決方案,以最小代價來維護代碼)。 記憶體的生命週期無論使用哪一種程式語言,記憶體的生命週期都是一樣的:##這裡簡單介紹一下記憶體生命週期中的每一個階段:
- 分配記憶體
- — 記憶體是由作業系統分配的,它允許您的程式使用它。在低階語言(例如C語言)中,這是一個開發人員需要自己處理的明確執行的操作。然而,在高階語言中,系統會自動為你分配內在。 使用記憶體
- — 這是程式實際使用先前分配的內存,在程式碼中使用分配的變數時,就會發生讀取和寫入操作。 釋放記憶體
- — 釋放所有不再使用的記憶體,使之成為自由記憶體,並且可以被重複使用。與分配記憶體操作一樣,這一操作在低階語言中也是需要明確地執行。 記憶體是什麼?
在介紹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中使用分配的記憶體意味著在其中讀寫,這可以透過讀取或寫入變量或物件屬性的值,或將參數傳遞給函數來實現。
當記憶體不再需要時進行釋放
大多數的記憶體管理問題都出現在這個階段
這裡最困難的地方是確定何時不再需要分配的內存,它通常要求開發人員確定程式中哪些地方不再需要內存的並釋放它。
高階語言嵌入了一種稱為垃圾收集器的機制,它的工作是追蹤記憶體分配和使用,以便發現任何時候一塊不再需要已分配的內在。在這種情況下,它將自動釋放這塊記憶體。
不幸的是,這個過程只是進行粗略估計,因為很難知道某塊內存是否真的需要 (不能通過算法來解決)。
大多數垃圾收集器透過收集不再被存取的記憶體來運作,例如,指向它的所有變數都超出了作用域。但是,這是可以收集的記憶體空間集合的一個不足估計值,因為在記憶體位置的任何一點上,仍然可能有一個變數在作用域中指向它,但是它將永遠不會再次存取。
垃圾收集
由於無法確定某些記憶體是否真的有用,因此,垃圾收集器想了一個辦法來解決這個問題。本節將解釋理解主要垃圾收集演算法及其限制。
記憶體引用
垃圾收集演算法主要依賴的是引用。
在記憶體管理上下文中,如果物件具有對另一個物件的存取權(可以是隱式的,也可以是顯式的),則稱物件引用另一個物件。例如,JavaScript物件具有對其原型(隱式參考)和屬性值(明確引用)的參考。
在此上下文中,「物件」的概念被擴展到比常規JavaScript物件更廣泛的範圍,並且還包含函數範圍(或全域詞法作用域)。
詞法作用域定義如何在巢狀函數中解析變數名:即使父函數已經傳回,內部函數也包含父函數的作用
引用計數垃圾收集演算法
#這是最簡單的垃圾收集演算法。如果沒有指向物件的引用,則認為該物件是「垃圾可回收的」,如下程式碼:
#循環會產生問題
當涉及到循環時,會有一個限制。在下面的範例中,創建了兩個物件,兩個物件互相引用,從而創建了一個循環。在函數呼叫之後將超出作用域,因此它們實際上是無用的,可以被釋放。然而,引用計數演算法認為,由於每個物件至少被引用一次,所以它們都不能被垃圾收集。
標記-清除(Mark-and-sweep)演算法
該演算法能夠判斷某個物件是否可以存取,從而知道該物件是否有用,該演算法由以下步驟組成:
- 垃圾收集器建立一個「根」列表,用於保存引用的全域變數。在JavaScript中,「window」物件是一個可作為根節點的全域變數。
- 然後,演算法檢查所有根及其子節點,並將它們標記為活動的(這意味著它們不是垃圾)。任何根不能到達的地方都會被標記為垃圾。
- 最後,垃圾收集器釋放所有未標記為活動的記憶體區塊,並將該記憶體傳回給作業系統。
這個演算法比上一個演算法要好,因為「一個物件沒有被引用」就意味著這個物件無法存取。
截至2012年,所有現代瀏覽器都有標記-清除垃圾收集器。過去幾年在JavaScript垃圾收集(分代/增量/並發/平行垃圾收集)領域所做的所有改進都是對該演算法(標記-清除)的實現改進,而不是對垃圾收集演算法本身的改進,也不是它決定物件是否可存取的目標。
在這篇文章中,你可以更詳細地閱讀到有關跟踪垃圾收集的詳細信息,同時還包括了標記-清除算法及其優化。
循環不再是問題
在上面的第一個例子中,在函數呼叫返回後,這兩個物件不再被從全域物件中可存取的物件引用。因此,垃圾收集器將發現它們無法存取。
儘管物件之間存在引用,但它們對於根節點來說是不可達的。
垃圾收集器的反直觀行為
儘管垃圾收集器很方便,但它們有一套自己的折衷方案,其中之一就是非決定論,換句話說,GC是不可預測的,你無法真正判斷何時進行垃圾收集。這意味著在某些情況下,程式會使用更多的記憶體,這實際上是必需的。在對速度特別敏感的應用程式中,可能會很明顯的感受到短時間的停頓。如果沒有分配記憶體,則大多數GC將處於空閒狀態。看看以下場景:
- 分配一組相當大的內在。
- 這些元素中的大多數(或全部)被標記為不可存取(假設引用指向一個不再需要的快取)。
- 不再進一步的分配
在這些場景中,大多數GCs 將不再繼續收集。換句話說,即使有不可訪問的引用可供收集,收集器也不會聲明這些引用。這些並不是嚴格意義上的洩漏,但仍然會導致比通常更高的記憶體使用。
記憶體洩漏是什麼?
從本質上說,記憶體洩漏可以定義為:不再被應用程式所需要的記憶體,出於某種原因,它不會回到操作系統或空閒記憶體池中。
程式語言支援不同的記憶體管理方式。然而,是否使用某一塊記憶體實際上是一個無法確定的問題。換句話說,只有開發人員才能明確指出一塊記憶體是否可以回到作業系統。
某些程式語言為開發人員提供了幫助,另一些則期望開發人員能清楚地了解記憶體何時不再被使用。維基百科上有一些有關人工和自動記憶體管理的很不錯的文章。
四種常見的記憶體洩漏
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)。幸運的是,現在大多數現代瀏覽器會為幫你完成這項工作:一旦觀察到的物件變得不可訪問,即使忘記刪除偵聽器,它們也會自動收集觀察者處理程序。然而,我們還是應該在物件被處理之前明確地刪除這些觀察者。例如:
如今,現在的瀏覽器(包括IE和Edge)使用現代的垃圾回收演算法,可以立即發現並處理這些循環引用。換句話說,在一個節點刪除之前也不是必須要呼叫removeEventListener。
一些框架或函式庫,例如JQuery,會在處置節點之前自動刪除監聽器(在使用它們特定的API的時候)。這是由庫內部的機制實現的,能夠確保不發生內存洩漏,即使在有問題的瀏覽器下運行也能這樣,比如……IE 6。
3.閉包
閉包是javascript開發的關鍵方面,一個內部函數使用了外部(封閉)函數的變數。由於JavaScript運行的細節,它可能以下面的方式造成記憶體洩漏:
#這段程式碼做了一件事:每次都呼叫replaceThing
#的時候,theThing
都會得到一個包含一個大數組和一個新閉包(someMethod)的新物件。同時,變數unuse
d指向一個引用了`originalThing
的閉包。
是不是有點困惑了? 重要的是,一旦具有相同父作用域的多個閉包的作用域被創建,則這個作用域就可以被共享。
在這種情況下,為閉包someMethod
而建立的作用域可以被unused
共享的。 unused
內部存在一個對originalThing
的引用。即使unused
從未使用過,someMethod
也可以在replaceThing
的作用域之外(例如在全域範圍內)透過theThing
來被調用。
由於someMethod
共享了unused
閉包的作用域,那麼unused
引用包含的originalThing
會迫使它保持活動狀態(兩個閉包之間的整個共享作用域)。這阻止了它被收集。
當這段程式碼重複運行時,可以觀察到記憶體使用在穩定增長,當GC
運行後,記憶體使用也不會變小。從本質上說,在運行過程中創建了一個閉包鍊錶(它的根是以變量theThing
的形式存在),並且每個閉包的作用域都間接引用了一個大數組,這造成了相當大的內存洩漏。
4.脫離DOM的引用
有時,將DOM節點儲存在資料結構中可能會很有用。假設你希望快速地更新表中的幾行內容,那麼你可以在一個字典或數組中保存每個DOM行的引用。這樣,同一個DOM元素就存在兩個引用:一個在DOM樹中,另一個則在字典中。如果在將來的某個時候你決定刪除這些行,那麼你需要將這兩個引用都設為不可訪問。
在引用 DOM 樹中的內部節點或葉節點時,還需要考慮另一個問題。如果在程式碼中保留對錶單元格的引用(
你可能認為垃圾收集器將釋放除該單元格之外的所有內容。然而,事實並非如此,由於單元格是表的一個子節點,而子節點保存對父節點的引用,所以對錶單元格的這個引用將使整個表保持在內存中,所以在移除有被引用的節點時候要移除其子節點。
以上是介紹JavaScript 記憶體管理+如何處理4個常見的記憶體洩漏的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

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

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

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

熱門話題

2022年3月3日,距離世界首個AI程式設計師Devin誕生不足一個月,普林斯頓大學的NLP團隊開發了一個開源AI程式設計師SWE-agent。它利用GPT-4模型在GitHub儲存庫中自動解決問題。 SWE-agent在SWE-bench測試集上的表現與Devin相似,平均耗時93秒,解決了12.29%的問題。 SWE-agent透過與專用終端交互,可以開啟、搜尋文件內容,使用自動語法檢查、編輯特定行,以及編寫和執行測試。 (註:以上內容為原始內容微調,但保留了原文中的關鍵訊息,未超過指定字數限制。)SWE-A

PHP與Vue:完美搭檔的前端開發利器在當今網路快速發展的時代,前端開發變得愈發重要。隨著使用者對網站和應用的體驗要求越來越高,前端開發人員需要使用更有效率和靈活的工具來創建響應式和互動式的介面。 PHP和Vue.js作為前端開發領域的兩個重要技術,搭配起來可以稱得上是完美的利器。本文將探討PHP和Vue的結合,以及詳細的程式碼範例,幫助讀者更好地理解和應用這兩

學習C語言的魅力:解鎖程式設計師的潛力隨著科技的不斷發展,電腦程式設計已經成為了一個備受關注的領域。在眾多程式語言中,C語言一直以來都備受程式設計師的喜愛。它的簡單、高效以及廣泛應用的特點,使得學習C語言成為了許多人進入程式設計領域的第一步。本文將討論學習C語言的魅力,以及如何透過學習C語言來解鎖程式設計師的潛力。首先,學習C語言的魅力在於其簡潔性。相較於其他程式語言而言,C語

在前端開發面試中,常見問題涵蓋廣泛,包括HTML/CSS基礎、JavaScript基礎、框架和函式庫、專案經驗、演算法和資料結構、效能最佳化、跨域請求、前端工程化、設計模式以及新技術和趨勢。面試官的問題旨在評估候選人的技術技能、專案經驗以及對行業趨勢的理解。因此,應試者應充分準備這些方面,以展現自己的能力和專業知識。

JavaScript教學:如何取得HTTP狀態碼,需要具體程式碼範例前言:在Web開發中,經常會涉及到與伺服器進行資料互動的場景。在與伺服器進行通訊時,我們經常需要取得傳回的HTTP狀態碼來判斷操作是否成功,並根據不同的狀態碼來進行對應的處理。本篇文章將教你如何使用JavaScript來取得HTTP狀態碼,並提供一些實用的程式碼範例。使用XMLHttpRequest

Django是一個由Python編寫的web應用框架,它強調快速開發和乾淨方法。儘管Django是web框架,但要回答Django是前端還是後端這個問題,需要深入理解前後端的概念。前端是指使用者直接和互動的介面,後端是指伺服器端的程序,他們透過HTTP協定進行資料的互動。在前端和後端分離的情況下,前後端程式可以獨立開發,分別實現業務邏輯和互動效果,資料的交

Go語言作為一種快速、高效的程式語言,在後端開發領域廣受歡迎。然而,很少有人將Go語言與前端開發聯繫起來。事實上,使用Go語言進行前端開發不僅可以提高效率,還能為開發者帶來全新的視野。本文將探討使用Go語言進行前端開發的可能性,並提供具體的程式碼範例,幫助讀者更了解這一領域。在傳統的前端開發中,通常會使用JavaScript、HTML和CSS來建立使用者介面

Golang與前端技術結合:探討Golang如何在前端領域發揮作用,需要具體程式碼範例隨著互聯網和行動應用的快速發展,前端技術也愈發重要。而在這個領域中,Golang作為一門強大的後端程式語言,也可以發揮重要作用。本文將探討Golang如何與前端技術結合,以及透過具體的程式碼範例來展示其在前端領域的潛力。 Golang在前端領域的角色作為一門高效、簡潔且易於學習的
