中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JavaScript如何處理內存泄漏

發布時間:2020-12-10 09:42:00 來源:億速云 閱讀:333 作者:小新 欄目:web開發

這篇文章主要介紹了JavaScript如何處理內存泄漏,具有一定借鑒價值,需要的朋友可以參考下。希望大家閱讀完這篇文章后大有收獲。下面讓小編帶著大家一起了解一下。

概述

像 C 這樣的編程語言,具有低級內存管理原語,如malloc()和free()。開發人員使用這些原語顯式地對操作系統的內存進行分配和釋放。

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

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

內存的生命周期

無論使用哪種編程語言,內存的生命周期都是一樣的:

JavaScript如何處理內存泄漏

這里簡單介紹一下內存生命周期中的每一個階段:

  • 分配內存 —? 內存是由操作系統分配的,它允許您的程序使用它。在低級語言(例如C語言)中,這是一個開發人員需要自己處理的顯式執行的操作。然而,在高級語言中,系統會自動為你分配內在。
  • 使用內存 — 這是程序實際使用之前分配的內存,在代碼中使用分配的變量時,就會發生讀和寫操作。
  • 釋放內存 — 釋放所有不再使用的內存,使之成為自由內存,并可以被重利用。與分配內存操作一樣,這一操作在低級語言中也是需要顯式地執行。

內存是什么?

在介紹JavaScript中的內存之前,我們將簡要討論內存是什么以及它是如何工作的。

硬件層面上,計算機內存由大量的觸發器緩存的。每個觸發器包含幾個晶體管,能夠存儲一位,單個觸發器都可以通過唯一標識符尋址,因此我們可以讀取和覆蓋它們。因此,從概念上講,可以把的整個計算機內存看作是一個可以讀寫的巨大數組。

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

很多東西都存儲在內存中:

  1. 程序使用的所有變量和其他數據。
  2. 程序的代碼,包括操作系統的代碼。

編譯器和操作系統一起為你處理大部分內存管理,但是你還是需要了解一下底層的情況,對內在管理概念會有更深入的了解。

在編譯代碼時,編譯器可以檢查基本數據類型,并提前計算它們需要多少內存。然后將所需的大小分配給調用堆棧空間中的程序,分配這些變量的空間稱為堆棧空間。因為當調用函數時,它們的內存將被添加到現有內存之上,當它們終止時,它們按照后進先出(LIFO)順序被移除。例如:

JavaScript如何處理內存泄漏

編譯器能夠立即知道所需的內存:4 + 4×4 + 8 = 28字節。

這段代碼展示了整型和雙精度浮點型變量所占內存的大小。但是大約20年前,整型變量通常占2個字節,而雙精度浮點型變量占4個字節。你的代碼不應該依賴于當前基本數據類型的大小。

編譯器將插入與操作系統交互的代碼,并申請存儲變量所需的堆棧字節數。

在上面的例子中,編譯器知道每個變量的確切內存地址。事實上,每當我們寫入變量 n 時,它就會在內部被轉換成類似“內存地址4127963”這樣的信息。

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

JavaScript如何處理內存泄漏

當函數調用其他函數時,每個函數在調用堆棧時獲得自己的塊。它保存所有的局部變量,但也會有一個程序計數器來記住它在執行過程中的位置。當函數完成時,它的內存塊將再次用于其他地方。

動態分配

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

JavaScript如何處理內存泄漏

在編譯時,編譯器不知道數組需要使用多少內存,因為這是由用戶提供的值決定的。

因此,它不能為堆棧上的變量分配空間。相反,我們的程序需要在運行時顯式地向操作系統請求適當的空間,這個內存是從堆空間分配的。靜態內存分配和動態內存分配的區別總結如下表所示:

靜態內存分配動態內存分配
大小必須在編譯時知道大小不需要在編譯時知道
在編譯時執行在運行時執行
分配給堆棧分配給堆
FILO (先進后出)沒有特定的分配順序

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

在JavaScript中分配內存

現在將解釋第一步:如何在JavaScript中分配內存。

JavaScript為讓開發人員免于手動處理內存分配的責任——JavaScript自己進行內存分配同時聲明值。

JavaScript如何處理內存泄漏

某些函數調用也會導致對象的內存分配:

JavaScript如何處理內存泄漏

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

JavaScript如何處理內存泄漏

在JavaScript中使用內存

在JavaScript中使用分配的內存意味著在其中讀寫,這可以通過讀取或寫入變量或對象屬性的值,或者將參數傳遞給函數來實現。

當內存不再需要時進行釋放

大多數的內存管理問題都出現在這個階段

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

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

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

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

垃圾收集

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

內存引用

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

在內存管理上下文中,如果對象具有對另一個對象的訪問權(可以是隱式的,也可以是顯式的),則稱對象引用另一個對象。例如,JavaScript對象具有對其原型(隱式引用)和屬性值(顯式引用)的引用。

在此上下文中,“對象”的概念被擴展到比常規JavaScript對象更廣泛的范圍,并且還包含函數范圍(或全局詞法作用域)。

詞法作用域定義了如何在嵌套函數中解析變量名:即使父函數已經返回,內部函數也包含父函數的作用

引用計數垃圾收集算法

這是最簡單的垃圾收集算法。如果沒有指向對象的引用,則認為該對象是“垃圾可回收的”,如下代碼:

JavaScript如何處理內存泄漏

循環會產生問題

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

JavaScript如何處理內存泄漏

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

該算法能夠判斷出某個對象是否可以訪問,從而知道該對象是否有用,該算法由以下步驟組成:

  1. 垃圾收集器構建一個“根”列表,用于保存引用的全局變量。在JavaScript中,“window”對象是一個可作為根節點的全局變量。
  2. 然后,算法檢查所有根及其子節點,并將它們標記為活動的(這意味著它們不是垃圾)。任何根不能到達的地方都將被標記為垃圾。
  3. 最后,垃圾收集器釋放所有未標記為活動的內存塊,并將該內存返回給操作系統。

JavaScript如何處理內存泄漏

這個算法比上一個算法要好,因為“一個對象沒有被引用”就意味著這個對象無法訪問。

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

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

循環不再是問題

在上面的第一個例子中,在函數調用返回后,這兩個對象不再被從全局對象中可訪問的對象引用。因此,垃圾收集器將發現它們不可訪問。

JavaScript如何處理內存泄漏

盡管對象之間存在引用,但它們對于根節點來說是不可達的。

垃圾收集器的反直觀行為

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

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

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

內存泄漏是什么?

從本質上說,內存泄漏可以定義為:不再被應用程序所需要的內存,出于某種原因,它不會返回到操作系統或空閑內存池中。

JavaScript如何處理內存泄漏

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

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

四種常見的內存泄漏

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如何處理內存泄漏

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

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

3.閉包

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

JavaScript如何處理內存泄漏

這段代碼做了一件事:每次調用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 樹中的內部節點或葉節點時,還需要考慮另外一個問題。如果在代碼中保留對表單元格的引用(<td>標記),并決定從 DOM 中刪除表,同時保留對該特定單元格的引用,那么可能會出現內存泄漏。

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

感謝你能夠認真閱讀完這篇文章,希望小編分享JavaScript如何處理內存泄漏內容對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,遇到問題就找億速云,詳細的解決方法等著你來學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

牙克石市| 安福县| 丹巴县| 扶风县| 陆河县| 淄博市| 洪泽县| 重庆市| 恩平市| 萨嘎县| 棋牌| 洛宁县| 沧州市| 万安县| 永胜县| 太谷县| 正镶白旗| 武强县| 大丰市| 福建省| 岳西县| 静乐县| 平湖市| 甘南县| 星座| 沙雅县| 许昌县| 青岛市| 吉林市| 古浪县| 吉安市| 彰武县| 江孜县| 胶南市| 江川县| 江山市| 独山县| 定州市| 高要市| 马尔康县| 东乌珠穆沁旗|