JavaScript 內(nèi)存管理

內(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);
  });
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末畔派,一起剝皮案震驚了整個(gè)濱河市铅碍,隨后出現(xiàn)的幾起案子润绵,更是在濱河造成了極大的恐慌线椰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尘盼,死亡現(xiàn)場(chǎng)離奇詭異憨愉,居然都是意外死亡常遂,警方通過查閱死者的電腦和手機(jī)胶背,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來审孽,“玉大人午阵,你說我怎么就攤上這事躺孝。” “怎么了底桂?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵植袍,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我籽懦,道長(zhǎng)于个,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任暮顺,我火速辦了婚禮厅篓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捶码。我一直安慰自己羽氮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布惫恼。 她就那樣靜靜地躺著乏苦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尤筐。 梳的紋絲不亂的頭發(fā)上汇荐,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音盆繁,去河邊找鬼掀淘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛油昂,可吹牛的內(nèi)容都是我干的革娄。 我是一名探鬼主播倾贰,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼拦惋!你這毒婦竟也來了匆浙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤厕妖,失蹤者是張志新(化名)和其女友劉穎首尼,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體言秸,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡软能,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了举畸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片查排。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖抄沮,靈堂內(nèi)的尸體忽然破棺而出跋核,到底是詐尸還是另有隱情,我是刑警寧澤叛买,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布砂代,位于F島的核電站,受9級(jí)特大地震影響聪全,放射性物質(zhì)發(fā)生泄漏泊藕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一难礼、第九天 我趴在偏房一處隱蔽的房頂上張望娃圆。 院中可真熱鬧,春花似錦蛾茉、人聲如沸讼呢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悦屏。三九已至,卻和暖如春键思,著一層夾襖步出監(jiān)牢的瞬間础爬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工吼鳞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留看蚜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓赔桌,卻偏偏與公主長(zhǎng)得像供炎,于是被迫代替她去往敵國(guó)和親渴逻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容