JavaScript內存管理介紹
本文已經(jīng)過原作者Ahmad shaded 授權翻譯。
大多數(shù)時候,我們在不了解有關內存管理的知識下也只開發(fā),因為 JS 引擎會為我們處理這個問題。不過,有時候我們會遇到內存泄漏之類的問題,這個只有知道內存分配是怎樣工作的,我們才能解決這些問題。
在本文中,主要介紹內存分配和垃圾回收的工作原理以及如何避免一些常見的內存泄漏問題。
緩存( Memory)生命周期
在 JS 中,當我們創(chuàng)建變量、函數(shù)或任何對象時,J S引擎會為此分配內存,并在不再需要時釋放它。
分配內存是在內存中保留空間的過程,而釋放內存則釋放空間,準備用于其他目的。
每次我們分配一個變量或創(chuàng)建一個函數(shù)時,該變量的存儲會經(jīng)歷以下相同的階段:
分配內存
- JS 會為我們處理這個問題:它分配我們創(chuàng)建對象所需的內存。
使用內存
- 使用內存是我們在代碼中顯式地做的事情:對內存的讀寫其實就是對變量的讀寫。
釋放內存
- 此步驟也由 JS 引擎處理,釋放分配的內存后,就可以將其用于新用途。
內存管理上下文中的“對象”不僅包括JS對象,還包括函數(shù)和函數(shù)作用域。
內存堆和堆
棧現(xiàn)在我們知道,對于我們在 JS 中定義的所有內容,引擎都會分配內存并在不再需要內存時將其釋放。
我想到的下一個問題是:這些東西將被儲存在哪里?
JS 引擎在兩個地方可以存儲數(shù)據(jù):內存堆和堆棧。堆和堆棧是引擎是用于不同目的的兩個數(shù)據(jù)結構。
堆棧:靜態(tài)內存分配
堆棧是 JS 用于存儲靜態(tài)數(shù)據(jù)的數(shù)據(jù)結構。靜態(tài)數(shù)據(jù)是引擎在編譯時能知道大小的數(shù)據(jù)。在 JS 中,包括指向對象和函數(shù)的原始值(strings,number,boolean,undefined和null)和引用類型。
由于引擎知道大小不會改變,因此它將為每個值分配固定數(shù)量的內存。
在執(zhí)行之前立即分配內存的過程稱為靜態(tài)內存分配。這些值和整個堆棧的限制取決于瀏覽器。
堆:動態(tài)內存分配
堆是另一個存儲數(shù)據(jù)的空間,JS 在其中存儲對象和函數(shù)。
與堆棧不同,JS 引擎不會為這些對象分配固定數(shù)量的內存,而根據(jù)需要分配空間。這種分配內存的方式也稱為動態(tài)內存分配。
下面將對這兩個存儲的特性進行比較:
堆棧 | 堆 |
---|---|
存放基本類型和引用 大小在編譯時已知 分配固定數(shù)量的內存 |
對象和函數(shù) 在運行時才知道大小 沒怎么限制 |
事例
來幾個事例,加強一下映像。
- const person = {
- name: 'John',
- age: 24,
- };
JS 在堆中為這個對象分配內存。實際值仍然是原始值,這就是它們存儲在堆棧中的原因。
- const hobbies = ['hiking', 'reading'];
數(shù)組也是對象,這就是為什么它們存儲在堆中的原因。
- let name = 'John'; // 為字符串分配內存
- const age = 24; // 為字分配內存
- name = 'John Doe'; // 為新字符串分配內存
- const firstName = name.slice(0,4); // 為新字符串分配內存
始值是不可變的,所以 JS 不會更改原始值,而是創(chuàng)建一個新值。
JavaScript 中的引用
所有變量首先指向堆棧。如果是非原始值,則堆棧包含對堆中對象的引用。
堆的內存沒有按特定的方式排序,所以我們需要在堆棧中保留對其的引用。我們可以將引用視為地址,并將堆中的對象視為這些地址所屬的房屋。
請記住,JS 將對象和函數(shù)存儲在堆中?;绢愋秃鸵么鎯υ诙褩V?。
這張照片中,我們可以觀察到如何存儲不同的值。注意person和newPerson都如何指向同一對象。
事例
- const person = {
- name: 'John',
- age: 24,
- };
這將在堆中創(chuàng)建一個新對象,并在堆棧中創(chuàng)建對該對象的引用。
垃圾回收
現(xiàn)在,我們知道 JS 如何為各種對象分配內存,但是在內存生命周期,還有最后一步:釋放內存。
就像內存分配一樣,JavaScript引擎也為我們處理這一步驟。更具體地說,垃圾收集器負責此工作。
一旦 JS 引擎識別變量或函數(shù)不在被需要時,它就會釋放它所占用的內存。
這樣做的主要問題是,是否仍然需要一些內存是一個無法確定的問題,這意味著不可能有一種算法能夠在不再需要那一刻立即收集不再需要的所有內存。
一些算法可以很好地解決這個問題。我將在本節(jié)中討論最常用的方法:引用計數(shù)和標記清除算法。
引用計數(shù)
當聲明了一個變量并將一個引用類型值賦值該變量時,則這個值的引用次數(shù)就是1。如果同一個值又被賦給另外一個變量,則該值得引用次數(shù)加1。相反,如果包含對這個值引用的變量又取 得了另外一個值,則這個值的引用次數(shù)減1。
當這個值的引用次數(shù)變成 0時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那 些引用次數(shù)為零的值所占用的內存。
我們看下面的例子。
請注意,在最后一幀中,只有hobbies留在堆中的,因為最后引用的是對象。
周期數(shù)
引用計數(shù)算法的問題在于它不考慮循環(huán)引用。當一個或多個對象互相引用但無法再通過代碼訪問它們時,就會發(fā)生這種情況。
- let son = {
- name: 'John',
- };
- let dad = {
- name: 'Johnson',
- }
- son.dad = dad;
- dad.son = son;
- son = null;
- dad = null;
由于父對象相互引用,因此該算法不會釋放分配的內存,我們再也無法訪問這兩個對象。
它們設置為null不會使引用計數(shù)算法識別出它們不再被使用,因為它們都有傳入的引用。
標記清除
標記清除算法對循環(huán)依賴性有解決方案。它檢測到是否可以從root 對象訪問它們,而不是簡單地計算對給定對象的引用。
瀏覽器的root是window 對象,而NodeJS中的root是global。
該算法將無法訪問的對象標記為垃圾,然后對其進行掃描(收集)。根對象將永遠不會被收集。
這樣,循環(huán)依賴關系就不再是問題了。在前面的示例中,dad對象和son對象都不能從根訪問。因此,它們都將被標記為垃圾并被收集。
自2012年以來,該算法已在所有現(xiàn)代瀏覽器中實現(xiàn)。僅對性能和實現(xiàn)進行了改進,算法的核心思想還是一樣的。
折衷
自動垃圾收集使我們可以專注于構建應用程序,而不用浪費時間進行內存管理。但是,我們需要權衡取舍。
內存使用
由于算法無法確切知道什么時候不再需要內存,JS 應用程序可能會使用比實際需要更多的內存。
即使將對象標記為垃圾,也要由垃圾收集器來決定何時以及是否將收集分配的內存。
如果你希望應用程序盡可能提高內存效率,那么最好使用低級語言。但是請記住,這需要權衡取舍。
性能
收集垃圾的算法通常會定期運行以清理未使用的對象。
問題是我們開發(fā)人員不知道何時會回收。收集大量垃圾或頻繁收集垃圾可能會影響性能。然而,用戶或開發(fā)人員通常不會注意到這種影響。
內存泄漏
在全局變量中存儲數(shù)據(jù),最常見內存問題可能是內存泄漏。
在瀏覽器的 JS 中,如果省略var,const或let,則變量會被加到window對象中。
- users = getUsers();
在嚴格模式下可以避免這種情況。
除了意外地將變量添加到根目錄之外,在許多情況下,我們需要這樣來使用全局變量,但是一旦不需要時,要記得手動的把它釋放了。
釋放它很簡單,把 null 給它就行了。
- window.users = null;
被遺忘的計時器和回調
忘記計時器和回調可以使我們的應用程序的內存使用量增加。特別是在單頁應用程序(SPA)中,在動態(tài)添加事件偵聽器和回調時必須小心。
被遺忘的計時器
- const object = {};
- const intervalId = setInterval(function() {
- // 這里使用的所有東西都無法收集直到清除`setInterval`
- doSomething(object);
- }, 2000);
上面的代碼每2秒運行一次該函數(shù)。如果我們的項目中有這樣的代碼,很有可能不需要一直運行它。
只要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引用
內存泄漏與前面的內存泄漏類似:它發(fā)生在用 JS 存儲DOM元素時。
- const elements = [];
- const element = document.getElementById('button');
- elements.push(element);
- function removeAllElements() {
- elements.forEach((item) => {
- document.body.removeChild(document.getElementById(item.id))
- });
- }
刪除這些元素時,我們還需要確保也從數(shù)組中刪除該元素。否則,將無法收集這些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元素也保留對其父節(jié)點的引用,因此可以防止垃圾收集器收集元素的父元素和子元素。
總結在本文中,我們總結了 JS 中內存管理的核心概念。寫這篇文章可以幫助我們理清一些我們不完全理解的概念。
希望這篇對你有所幫助,我們下期再見,記得三連哦!
作者:Ahmad shaded 譯者:前端小智 來源:felixgerschau
原文:https://felixgerschau.com/javascript-memory-management/
本文轉載自微信公眾號「大遷世界」,可以通過以下二維碼關注。轉載本文請聯(lián)系大遷世界公眾號。