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