【文末送書】JavaScript內存管理介紹

2021-02-20 大遷世界

本文已經過原作者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 中內存管理的核心概念。寫這篇文章可以幫助我們理清一些我們不完全理解的概念。

希望這篇對你有所幫助,我們下期再見,記得三連哦!

作者:Ahmad shaded  譯者:前端小智  來源:felixgerschau

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


Web前端工程師修煉之道(第5版)》

相關焦點

  • JavaScript內存管理機制以及四種常見的內存洩漏解析
    本系列的第一篇文章簡單介紹了引擎、運行時間和堆棧的調用。第二篇文章研究了谷歌V8 JavaScript引擎的內部機制,並介紹了一些編寫JavaScript代碼的技巧。而這第三篇文章將討論另一個很重要的主題——內存管理。隨著程式語言變得越來越成熟越來越複雜,開發人員很容易忽視這一問題。
  • C++機器學習庫介紹 | 文末送書
    介紹我喜歡使用C++。
  • 七天學會javascript第一天javascript介紹
    前幾周寫了幾篇關於入門php的文章,反響還不錯,之前簡單的提到了JavaScript,這周小編重點介紹JavaScript讓大家可以在一周時間內掌握這門前端語言的基本用法。javascript介紹javascript數據類型javascript運算符javascript對象javascript
  • 萬字長文!別再說你不懂 Linux 內存管理了
    需要高清示例圖片的同學,文末有獲取方式自取。再功利點的說,面試的時候不經意間透露你懂這方面知識,並且能說出個一二三來,也許能讓面試官對你更有興趣,離升職加薪,走上人生巔峰又近了一步。內核空間用戶空間全圖內存數據結構要讓內核管理系統中的虛擬內存,必然要從中抽象出內存管理數據結構,內存管理操作如「分配、釋放等」都基於這些數據結構操作,這裡列舉兩個管理虛擬內存區域的數據結構
  • 學好Python,必須熟練掌握的幾種數據結構【文末送書】
    python提供了多種數據結構可供選擇,除了全局的列表、字典、集合和元組4個基本類型外,collections模塊提供了一些定製化的數據結構集合類數據結構,array和heapq模塊則分別提供了數組和堆數據結構,本文就這4種類型加以分別介紹。本文所指數據結構特指容器類數據結構,不包含int、str、boolean等單數據類型。
  • 【郝萬山教授講傷寒】太陽病的證候分類和治法(文末送郝萬山籤名書)
    《郝萬山傷寒論講稿》領取方式看文末從太陽病的證候分類來說,總體上我們分了三大類,一類是本證,一類是變證,一類是類證。因為太陽主表而統營衛,它是管理汗孔開合、調節體溫的,所以太陽被風寒邪氣所傷,它管理汗孔開合這種功能受到了影響,有的時候汗了開而不合就表現為有汗,有的時候汗了閉而不開,就表現為無汗。有汗的把它叫做太陽中風證,無汗的把它叫做太陽傷寒證。太陽中風在治療上用桂枝湯,桂枝湯這張方子我們在學《中藥學》的時候,學《方劑學》的時候,都學到過。
  • 詳解 JavaScript 閉包
    ,會增大內存使用量,使用不當很容易造成內存洩露。閉包是javascript語言的一大特點,主要應用閉包場合主要是為了:設計私有的方法和變量。一般函數執行完畢後,局部活動對象就被銷毀,內存中僅僅保存全局作用域。但閉包的情況不同!
  • Android 內存管理詳解
    和你一起終身學習,這裡是程式設計師Android經典好文推薦,通過閱讀本文,您將收穫以下知識點:一、Android 垃圾回收機制(GC)二、共享內存三、內存的申請與回收四、內存限制五、不同App切換時的內存管理Android Runtime(ART)和Dalvik虛擬機使用 分頁 和 內存映射 來管理內存。
  • 理解 Go 內存管理之內存分配
    mheap的作用不只是管理central,另外大對象也會直接通過mheap進行分配。mheap實現了對於虛擬內存線性地址空間的精準管理,建立了span與具體線性地址空間的聯繫,保存了分配的位圖信息,是管理內存的最核心單元。
  • Linux堆內存管理深入分析(二)
    Arena Header包含bins的信息、top chunk以及最後一個remainder chunk等(這些概念會在後文詳細介紹):struct malloc_state{  /* Serialize access.  */  mutex_t mutex;  /* Flags (formerly in max_fast).  */  int flags;  /* Fastbins
  • JavaScript中的內存釋放
    01如何查找上級作用域在《JavaScript中的預解析》,有講到作用域鏈的概念,本次在正式講JavaScript中的內存釋放之前
  • 萬字長文,別再說你不懂Linux內存管理了(合輯),30 張圖給你安排的明明白白
    需要高清示例圖片的同學,文末有獲取方式自取。再功利點的說,面試的時候不經意間透露你懂這方面知識,並且能說出個一二三來,也許能讓面試官對你更有興趣,離升職加薪,走上人生巔峰又近了一步。內核空間用戶空間全圖內存數據結構要讓內核管理系統中的虛擬內存,必然要從中抽象出內存管理數據結構,內存管理操作如「分配、釋放等」都基於這些數據結構操作,這裡列舉兩個管理虛擬內存區域的數據結構
  • 286本的編程電子書合集,多年珍藏電子書資源(免費送
    電子書大家都懂的,不管你存多少本,都只佔內存,不佔空間。有了它,至少搬家再也不用愁書沒地方放和搬不動了![領取方式在文章最後,完全免費的哦,先到先得]今天給大家分享的是我收集了2年,價值上萬元的286本編程方面最權威電子書,
  • 每日一課 | 使用JavaScript強制下載(文末免費領好課)
    翻譯自: https://davidwalsh.name/javascript-download今日福利👇
  • 諸葛亮 VS 龐統,拿下分布式 Paxos | 文末送書
    比如 Raft 算法(後面也會介紹)。所以學習分布式算法必須先學習 Paxos 算法。Paxos 算法主要包含兩個部分:Basic Paxos 算法:多個節點之間如何就某個值達成共識。劉備集團提議者(Proposer)接入和協調,收到客戶端的請求後,可以發起二階段提交,進行共識協商。
  • 最強大、最牛逼的javascript視頻免費發布啦
    是一門非常強大的腳本語言,應用的範圍非常廣泛,每一個web開發者學好javascript也是必須的,本套視頻教程詳細的講解了javascript各個知識點、關鍵點,其中涉及到高深的函數概念、原型概念、接口概念、單體概念、更是詳細的講解了javascript設計模式。 本
  • 一文讀懂 javascript 深拷貝與淺拷貝
    (給前端大全加星標,提升前端技能)作者:前端工匠 公號 /  浪裡行舟君 (本文來自作者投稿)前言在 javascri
  • JavaScript 的 this 原理
    (點擊上方公眾號,可快速關注)作者:阮一峰www.ruanyifeng.com/blog/2018/06/javascript-this.html
  • 如何讓手機內存變大 手機釋放內存的方法介紹
    手機內存介紹  1、運行內存,即隨機存儲器,英文縮寫為RAM,相當於電腦的內存條,出廠多大就多大,不能擴充容量,一般是1G或2G為主,不超過4GB;手機自帶內存設備是 焊接 在手機主板上的,所以說一般人要增加機身內存難度很大,可以說是不現實的,如果真的要這麼做,建議你找到生產這部手機的生產工廠,也就是往手機主板上焊接內存的生產線,找具備更換機身內存能力的工人或者技師幫你更換一個同規格大容量的內存,這樣就可以使你的手機自帶內存變大了。
  • 分享10款超酷的Javascript繪畫和畫布類庫
    gt; 資源 > 關鍵詞 > mvc最新資訊 > 正文 分享10款超酷的Javascript繪畫和畫布類庫 javascript