內(nèi)存生命周期
在 JavaScript 中寞秃,當(dāng)我們創(chuàng)建變量斟叼、函數(shù)或者其他東西的時(shí)候,JS 引擎會(huì)自動(dòng)的為它分配內(nèi)存蜕该,當(dāng)它不再被使用的時(shí)候犁柜,JS 引擎又會(huì)自動(dòng)的去釋放掉這塊內(nèi)存洲鸠。
分配內(nèi)存堂淡,實(shí)際上是在內(nèi)存中保留一塊空間的過程,而 釋放內(nèi)存 則是釋放這塊區(qū)域的空間扒腕,以便后續(xù)使用绢淀。
每次我們給變量賦值或者創(chuàng)建一個(gè)函數(shù)的時(shí)候,它所對(duì)應(yīng)的那塊內(nèi)存總會(huì)經(jīng)歷如下的階段:
- 內(nèi)存分配
JavaScript 會(huì)幫我們處理瘾腰,它會(huì)為我們創(chuàng)建的內(nèi)容分配內(nèi)存皆的。 - 內(nèi)存使用
使用內(nèi)存的過程體現(xiàn)在代碼中,我們對(duì)于變量或?qū)ο蟮鹊淖x寫其實(shí)就是對(duì)內(nèi)存的讀寫蹋盆。 - 內(nèi)存釋放
這一步也是由 JavaScript 引擎處理的费薄。一旦這個(gè)內(nèi)存被釋放掉了硝全,它就可以用于新的目的。
堆內(nèi)存和棧內(nèi)存
現(xiàn)在我們知道了楞抡,在 JavaScript 中定義的任何東西伟众,JS 引擎都會(huì)為他分配內(nèi)存,并且在不再使用的時(shí)候釋放掉召廷。
接下來我們要考慮的問題就是:我們創(chuàng)建的變量凳厢、函數(shù)等,會(huì)被存放在哪里呢竞慢?
JavaScript 引擎有兩個(gè)地方可以存儲(chǔ)數(shù)據(jù):堆內(nèi)存 和 棧內(nèi)存 先紫。
堆(Heap) 和 棧(Stack) 是兩種不同的數(shù)據(jù)結(jié)構(gòu),他們的使用場(chǎng)景也各不相同筹煮。
1. 棧:靜態(tài)內(nèi)存分配
棧是 JavaScript 用來存放 靜態(tài)數(shù)據(jù) 的一種數(shù)據(jù)結(jié)構(gòu)遮精。靜態(tài)數(shù)據(jù)指的是 JS 引擎在編譯時(shí)期就能確定其大小的數(shù)據(jù)。在 JS 中败潦,它包括 原始的值(strings, numbers, booleans, undefined, symbol, and null)和 指向?qū)ο蠛秃瘮?shù)的 引用仑鸥。
由于引擎知道了數(shù)據(jù)的大小不會(huì)再改變了,那么在分配內(nèi)存的時(shí)候变屁,就會(huì)給它分配一個(gè) 固定大小 的空間眼俊。
在程序執(zhí)行前分配內(nèi)存的過程,就叫做 靜態(tài)內(nèi)存分配粟关。
因?yàn)橐鏋檫@些值分配的是固定大小的內(nèi)存疮胖,所以這些值的大小肯定是有個(gè)上限的,而這個(gè)上限取決于具體的瀏覽器闷板。
2. 堆:動(dòng)態(tài)內(nèi)存分配
堆內(nèi)存是 JavaScript 用來存在對(duì)象和函數(shù)的區(qū)域澎灸。與棧內(nèi)存不同的是,引擎并不會(huì)為這些對(duì)象分配一個(gè)固定大小的內(nèi)存遮晚,相反性昭,它將根據(jù)具體的需要來分配對(duì)應(yīng)的內(nèi)存空間,這種內(nèi)存分配的方式就是 動(dòng)態(tài)內(nèi)存分配 县遣。
我們來對(duì)比一下棧和堆內(nèi)存的區(qū)別:
棧(Stack) | 堆(Heap) | |
---|---|---|
值類型 | 原始值和引用 | 對(duì)象和函數(shù) |
時(shí)期 | 編譯期間確定大小 | 運(yùn)行期間確定大小 |
大小 | 固定大小 | 無具體限制 |
// 為對(duì)象分配堆內(nèi)存
const person = {
name: 'John',
age: 24,
};
// 數(shù)組也是對(duì)象糜颠,所以分配的也是堆內(nèi)存
const hobbies = ['hiking', 'reading'];
let name = 'John'; // 為字符串分配棧內(nèi)存
const age = 24; // 為數(shù)字分配棧內(nèi)存
name = 'John Doe'; // 為字符串分配新的棧內(nèi)存
const firstName = name.slice(0,4); // 為字符串分配新的棧內(nèi)存
這里要注意的是,原始值都是不可變的萧求,所以修改的時(shí)候?qū)嶋H上是創(chuàng)建了一個(gè)新的值其兴。
JavaScript 中的引用
所有的變量一開始都是指向棧的。如果它不是原始值夸政,那么棧中保留著指向堆內(nèi)存中對(duì)象的引用元旬。
堆內(nèi)存里的數(shù)據(jù)并不是按照某個(gè)特定的順序排列的,所以我們需要在棧中保留一個(gè)指向堆內(nèi)存數(shù)據(jù)的引用。您可以將引用當(dāng)作是地址匀归,而堆內(nèi)存中的對(duì)象則是這些地址所對(duì)應(yīng)的房屋坑资。
上圖清晰的展示了不同類型的值是如何存放的。要注意的是穆端,person 和 newPerson 都是指向同一個(gè)對(duì)象的盐茎。
1. 垃圾收集
這里已經(jīng)知道了,JavaScript 會(huì)為所有類型的數(shù)據(jù)分配內(nèi)存徙赢,但是如果你還記得一開始介紹的內(nèi)存生命周期字柠,你就知道我們還缺少最后一步:內(nèi)存釋放。
與內(nèi)存分配一樣狡赐,這一步也是由 JS 引擎為我們完成的窑业,更具體的說,是 垃圾收集器 為我們完成的枕屉。
當(dāng) JS 引擎識(shí)別到給定變量或函數(shù)不再需要的時(shí)候常柄,它就會(huì)釋放其所占用的內(nèi)存。
這一步驟的主要問題在于搀擂,我們無法精確的判定某一塊內(nèi)存是仍然需要的西潘,這 只能是一個(gè)近似的過程,無法通過算法來解決哨颂。這里介紹兩種最常見的算法:引用計(jì)數(shù)法 和 標(biāo)記清除法(注意喷市,它們也都是最大程度的近似判定)。
2. 引用計(jì)數(shù)法
這是最簡(jiǎn)單的實(shí)現(xiàn)威恼,它收集 沒有引用指向它們的 對(duì)象作為垃圾品姓。來看一下下面的演示:
這里要注意,在最后一幀中箫措,只有 hobbies 保留在堆內(nèi)存中腹备,因?yàn)樗俏ㄒ灰粋€(gè)有引用指向他的對(duì)象。
循環(huán)引用
引用計(jì)數(shù)法的問題在于斤蔓,它沒有考慮到循環(huán)引用的場(chǎng)景植酥。當(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;
由于 son 和 dad 這兩個(gè)對(duì)象都引用了對(duì)方喇伯,所以這個(gè)算法不會(huì)釋放它們占用的內(nèi)存喊儡,我們也無法通過代碼來訪問到這兩個(gè)對(duì)象拨与。將它們都設(shè)置為 null 也無濟(jì)于事稻据,因?yàn)槎加幸弥赶蛩鼈儯詷?biāo)記清除法照樣會(huì)認(rèn)為它們是有用的,不可回收捻悯。
3. 標(biāo)記清除法
標(biāo)記清除法很好的避免了循環(huán)引用的問題匆赃。它假定了一個(gè)叫做根(root)的對(duì)象,然后從它出發(fā)去訪問給定的對(duì)象今缚。根對(duì)象在瀏覽器中是 window 對(duì)象算柳,在 NodeJS 中是 global 對(duì)象。
該算法將 不可訪問的對(duì)象 標(biāo)記為垃圾姓言,然后 清除(收集)它們瞬项。根對(duì)象將永遠(yuǎn)不會(huì)被收集。這樣何荚,循環(huán)引用就不再是個(gè)問題了囱淋。在之前的例子中,dad 和 son 這兩個(gè)對(duì)象最后都無法通過根對(duì)象訪問到餐塘,所以它們都會(huì)被標(biāo)記為垃圾然后被清理掉妥衣。
從2012年起,所有現(xiàn)代瀏覽器都使用了標(biāo)記-清除垃圾回收算法戒傻。所有對(duì) JavaScript 垃圾回收算法的改進(jìn)都是基于標(biāo)記-清除算法性能和實(shí)現(xiàn)的改進(jìn)税手,并不是對(duì)算法本身。
權(quán)衡
自動(dòng)的垃圾收集機(jī)制讓我們可以專注于構(gòu)建應(yīng)用程序本身需纳,而不用因?yàn)閮?nèi)存管理而浪費(fèi)時(shí)間芦倒。然而,我們需要注意一些權(quán)衡不翩。
1. 內(nèi)存使用
由于算法無法確切的知道何時(shí)不再需要某塊內(nèi)存熙暴,所以 Javascript 應(yīng)用可能會(huì)比平時(shí)需要更多的內(nèi)存。
即使某些對(duì)象已經(jīng)被標(biāo)記為垃圾了慌盯,但具體的垃圾收集時(shí)機(jī)還是由垃圾收集器來決定的周霉。
如果你想你的應(yīng)用程序盡可能地提高內(nèi)存效率,那你最好使用一些底層(lower-level)語言亚皂。但請(qǐng)記住俱箱,任何語言的內(nèi)存管理都有自己的一套權(quán)衡。
2. 性能
為我們收集垃圾的算法通常是定期運(yùn)行的灭必。然而問題是狞谱,作為開發(fā)者,我們并不知道它什么時(shí)候發(fā)生禁漓。收集大量的垃圾或頻繁地收集垃圾可能會(huì)影響性能跟衅,因?yàn)檫@樣做需要一定的計(jì)算能力。當(dāng)然播歼,我們的用戶或開發(fā)人員通常不會(huì)注意到這種影響伶跷。
內(nèi)存泄漏
好了,有了上面的知識(shí)儲(chǔ)備,下面我們來看看幾種常見的內(nèi)存泄漏問題叭莫。當(dāng)你理解背后的原理時(shí)蹈集,你就會(huì)發(fā)現(xiàn)這些問題都可以輕松的避免。
1. 全局對(duì)象
將數(shù)據(jù)存儲(chǔ)在全局變量上可能是最常見的內(nèi)存泄漏問題了雇初。舉個(gè)例子拢肆,在瀏覽器中聲明一個(gè)變量,如果你不用 const 或者 let靖诗,而是用 var 或者干脆省略關(guān)鍵字郭怪,那么這個(gè)變量將會(huì)變成 window 對(duì)象的一個(gè)屬性。用 function 定義的函數(shù)也同理刊橘。
major = 'JS';
var user = 'Jerry';
function getName() {
return 'jerry';
}
window.major // => 'JS'
window.user // => 'Jerry'
window.getName() // => 'jerry'
這只適用于在全局作用域中定義的變量和函數(shù)移盆,關(guān)于 JS 作用域的內(nèi)容你可以參考這篇文章。
你可以在 嚴(yán)格模式 下運(yùn)行你的代碼伤为,這樣可以避免上述問題咒循。
當(dāng)然有時(shí)候你可能是故意的使用全局變量來存儲(chǔ)一些信息,但是請(qǐng)確保在不再需要這些對(duì)象的時(shí)候主動(dòng)的設(shè)置為 null绞愚,這樣可以保證垃圾收集器可以及時(shí)的回收掉它的內(nèi)存:
window.user = null;
2. 被遺忘的定時(shí)器與回調(diào)函數(shù)
忘記處理了某些計(jì)時(shí)器和回調(diào)函數(shù)會(huì)增加應(yīng)用程序的內(nèi)存叙甸。特別是在單頁應(yīng)用程序(SPA)中,在動(dòng)態(tài)添加事件監(jiān)聽和回調(diào)時(shí)務(wù)必要小心位衩。
定時(shí)器
const object = {};
const intervalId = setInterval(function() {
doSomething(object);
}, 2000);
這段代碼每?jī)擅雸?zhí)行一次裆蒸,定時(shí)器內(nèi)部引用了外部的 object 對(duì)象。只要定時(shí)器在運(yùn)行糖驴,這個(gè) object 對(duì)象就不會(huì)被回收僚祷。所以要確保在合適的時(shí)機(jī)清除掉這個(gè)定時(shí)器:
clearInterval(intervalId);
這點(diǎn)在 SPA 中特別重要。因?yàn)橛袝r(shí)候你可能已經(jīng)導(dǎo)航到另一個(gè)頁面去了贮缕,但是原先頁面的定時(shí)器還在后臺(tái)運(yùn)行著辙谜,它導(dǎo)致了引用了外部對(duì)象無法被回收。
回調(diào)函數(shù)
假設(shè)你有一個(gè)按鈕感昼,它綁定了一個(gè) onclick 事件装哆。
一些老的瀏覽器的垃圾回收器是無法收集監(jiān)聽器的,不過現(xiàn)在基本都可以了定嗓,不過還是建議你在不需要的時(shí)候蜕琴,手動(dòng)的移除事件監(jiān)聽,釋放內(nèi)存宵溅。
const element = document.getElementById('button');
const onClick = () => alert('hi');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
DOM 引用
這種內(nèi)存泄漏與上一個(gè)相似凌简,它們都發(fā)生在存儲(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))
});
}
當(dāng)你刪除某一個(gè)元素的時(shí)候恃逻,你可能希望從 elements 數(shù)組中也刪除對(duì)應(yīng)的元素雏搂。否則藕施,這些 DOM 元素還是不能被垃圾收集器收集。
const elements = [];
const element = document.getElementById('button');
elements.push(element);
function removeAllElements() {
elements.forEach((item, index) => {
document.body.removeChild(document.getElementById(item.id));
// 從數(shù)組中刪除
elements.splice(index, 1);
});
}