首頁>技術>

本文已經過原作者Ahmad shaded 授權翻譯。

大多數時候,我們在不瞭解有關記憶體管理的知識下也只開發,因為 JS 引擎會為我們處理這個問題。不過,有時候我們會遇到記憶體洩漏之類的問題,這個只有知道記憶體分配是怎樣工作的,我們才能解決這些問題。

在本文中,主要介紹記憶體分配垃圾回收的工作原理以及如何避免一些常見的記憶體洩漏問題。

快取( Memory)生命週期

在 JS 中,當我們建立變數、函式或任何物件時,J S引擎會為此分配記憶體,並在不再需要時釋放它。

分配記憶體是在記憶體中保留空間的過程,而釋放記憶體則釋放空間,準備用於其他目的。

每次我們分配一個變數或建立一個函式時,該變數的儲存會經歷以下相同的階段:

分配記憶體

JS 會為我們處理這個問題:它分配我們建立物件所需的記憶體。

使用記憶體

使用記憶體是我們在程式碼中顯式地做的事情:對記憶體的讀寫其實就是對變數的讀寫。

釋放記憶體

此步驟也由 JS 引擎處理,釋放分配的記憶體後,就可以將其用於新用途。

記憶體管理上下文中的“物件”不僅包括JS物件,還包括函式和函式作用域。

記憶體堆和堆疊

現在我們知道,對於我們在 JS 中定義的所有內容,引擎都會分配記憶體並在不再需要記憶體時將其釋放。

我想到的下一個問題是:這些東西將被儲存在哪裡?

JS 引擎在兩個地方可以儲存資料:記憶體堆堆疊。堆和堆疊是引擎是用於不同目的的兩個資料結構。

堆疊:靜態記憶體分配

堆疊是 JS 用於儲存靜態資料的資料結構。靜態資料是引擎在編譯時能知道大小的資料。在 JS 中,包括指向物件和函式的原始值(strings,number,boolean,undefined和null)和引用型別。

由於引擎知道大小不會改變,因此它將為每個值分配固定數量的記憶體。

在執行之前立即分配記憶體的過程稱為靜態記憶體分配。這些值和整個堆疊的限制取決於瀏覽器。

堆:動態記憶體分配

是另一個儲存資料的空間,JS 在其中儲存物件函式

與堆疊不同,JS 引擎不會為這些物件分配固定數量的記憶體,而根據需要分配空間。這種分配記憶體的方式也稱為動態記憶體分配

下面將對這兩個儲存的特性進行比較:

堆疊堆存放基本型別和引用大小在編譯時已知分配固定數量的記憶體物件和函式在執行時才知道大小沒怎麼限制

事例

來幾個事例,加強一下映像。

const person = {  name: 'John',  age: 24,};

JS 在堆中為這個物件分配記憶體。實際值仍然是原始值,這就是它們儲存在堆疊中的原因。

const hobbies = ['hiking', 'reading'];

陣列也是物件,這就是為什麼它們儲存在堆中的原因。

let name = 'John'; // 為字串分配記憶體const age = 24; // 為字分配記憶體name = 'John Doe'; // 為新字串分配記憶體const firstName = name.slice(0,4); // 為新字串分配記憶體

始值是不可變的,所以 JS 不會更改原始值,而是建立一個新值。

JavaScript 中的引用

所有變數首先指向堆疊。如果是非原始值,則堆疊包含對堆中物件的引用。

堆的記憶體沒有按特定的方式排序,所以我們需要在堆疊中保留對其的引用。我們可以將引用視為地址,並將堆中的物件視為這些地址所屬的房屋。

請記住,JS 將物件函式儲存在堆中。基本型別和引用儲存在堆疊中。

這張照片中,我們可以觀察到如何儲存不同的值。注意person和newPerson都如何指向同一物件。

事例
const person = {  name: 'John',  age: 24,};

這將在堆中建立一個新物件,並在堆疊中建立對該物件的引用。

垃圾回收

現在,我們知道 JS 如何為各種物件分配記憶體,但是在記憶體生命週期,還有最後一步:釋放記憶體

就像記憶體分配一樣,JavaScript引擎也為我們處理這一步驟。更具體地說,垃圾收集器負責此工作。

一旦 JS 引擎識別變數或函式不在被需要時,它就會釋放它所佔用的記憶體。

這樣做的主要問題是,是否仍然需要一些記憶體是一個無法確定的問題,這意味著不可能有一種演算法能夠在不再需要那一刻立即收集不再需要的所有記憶體。

引用計數

當聲明瞭一個變數並將一個引用型別值賦值該變數時,則這個值的引用次數就是1。如果同一個值又被賦給另外一個變數,則該值得引用次數加1。相反,如果包含對這個值引用的變數又取 得了另外一個值,則這個值的引用次數減1。

當這個值的引用次數變成 0時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的記憶體空間回收回來。這樣,當垃圾收集器下次再執行時,它就會釋放那 些引用次數為零的值所佔用的記憶體。

我們看下面的例子。

請注意,在最後一幀中,只有hobbies留在堆中的,因為最後引用的是物件。

週期數

引用計數演算法的問題在於它不考慮迴圈引用。當一個或多個物件互相引用但無法再透過程式碼訪問它們時,就會發生這種情況。

let son = {  name: 'John',};let dad = {  name: 'Johnson',}son.dad = dad;dad.son = son;son = null;dad = null;

由於父物件相互引用,因此該演算法不會釋放分配的記憶體,我們再也無法訪問這兩個物件。

它們設定為null不會使引用計數演算法識別出它們不再被使用,因為它們都有傳入的引用。

標記清除

標記清除演算法對迴圈依賴性有解決方案。它檢測到是否可以從root 物件訪問它們,而不是簡單地計算對給定物件的引用。

瀏覽器的root是window 物件,而NodeJS中的root是global。

該演算法將無法訪問的物件標記為垃圾,然後對其進行掃描(收集)。根物件將永遠不會被收集。

這樣,迴圈依賴關係就不再是問題了。在前面的示例中,dad物件和son物件都不能從根訪問。因此,它們都將被標記為垃圾並被收集。

自2012年以來,該演算法已在所有現代瀏覽器中實現。僅對效能和實現進行了改進,演算法的核心思想還是一樣的。

折衷

自動垃圾收集使我們可以專注於構建應用程式,而不用浪費時間進行記憶體管理。但是,我們需要權衡取捨。

記憶體使用

由於演算法無法確切知道什麼時候不再需要記憶體,JS 應用程式可能會使用比實際需要更多的記憶體。

即使將物件標記為垃圾,也要由垃圾收集器來決定何時以及是否將收集分配的記憶體。

如果你希望應用程式儘可能提高記憶體效率,那麼最好使用低階語言。但是請記住,這需要權衡取捨。

效能

收集垃圾的演算法通常會定期執行以清理未使用的物件。

問題是我們開發人員不知道何時會回收。收集大量垃圾或頻繁收集垃圾可能會影響效能。然而,使用者或開發人員通常不會注意到這種影響。

記憶體洩漏

在全域性變數中儲存資料,最常見記憶體問題可能是記憶體洩漏

在瀏覽器的 JS 中,如果省略var,const或let,則變數會被加到window物件中。

users = getUsers();

在嚴格模式下可以避免這種情況。

除了意外地將變數新增到根目錄之外,在許多情況下,我們需要這樣來使用全域性變數,但是一旦不需要時,要記得手動的把它釋放了。

釋放它很簡單,把 null 給它就行了。

window.users = null;
被遺忘的計時器和回撥

忘記計時器和回撥可以使我們的應用程式的記憶體使用量增加。特別是在單頁應用程式(SPA)中,在動態新增事件偵聽器和回撥時必須小心。

被遺忘的計時器
const object = {};const intervalId = setInterval(function() {  // 這裡使用的所有東西都無法收集直到清除`setInterval`  doSomething(object);}, 2000);

上面的程式碼每2秒執行一次該函式。如果我們的專案中有這樣的程式碼,很有可能不需要一直執行它。

只要setInterval沒有被取消,則其中的引用物件就不會被垃圾回收。

確保在不再需要時清除它。

clearInterval(intervalId);
被遺忘的回撥

假設我們向按鈕添加了onclick偵聽器,之後該按鈕將被刪除。舊的瀏覽器無法收集偵聽器,但是如今,這不再是問題。

const element = document.getElementById('button');const onClick = () => alert('hi');element.addEventListener('click', onClick);element.removeEventListener('click', onClick);element.parentNode.removeChild(element);
脫離DOM引用

記憶體洩漏與前面的記憶體洩漏類似:它發生在用 JS 儲存DOM元素時。

const elements = [];const element = document.getElementById('button');elements.push(element);function removeAllElements() {  elements.forEach((item) => {    document.body.removeChild(document.getElementById(item.id))  });}

刪除這些元素時,我們還需要確保也從陣列中刪除該元素。否則,將無法收集這些DOM元素。

const elements = [];const element = document.getElementById('button');elements.push(element);function removeAllElements() {  elements.forEach((item, index) => {    document.body.removeChild(document.getElementById(item.id));    elements.splice(index, 1);  });}

由於每個DOM元素也保留對其父節點的引用,因此可以防止垃圾收集器收集元素的父元素和子元素。

總結

在本文中,我們總結了 JS 中記憶體管理的核心概念。寫這篇文章可以幫助我們理清一些我們不完全理解的概念。

原文:https://felixgerschau.com/javascript-memory-management/

17
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • JavaScript學習筆記(十八)ES5