淺談 JavaScript 內(nèi)存管理與垃圾回收

寫(xiě)過(guò)C語(yǔ)言的都清楚,我們需要時(shí)時(shí)刻刻關(guān)心處理程序的內(nèi)存使用情況,這無(wú)形的給程序員增添了很多負(fù)擔(dān),但是在后期出現(xiàn)的一些語(yǔ)言中漸漸的都加入了內(nèi)存自動(dòng)管理和垃圾回收機(jī)制,這樣一來(lái)我們就不必再關(guān)心程序運(yùn)行的內(nèi)存使用情況,同樣的在JavaScript中也有內(nèi)存管理和垃圾回收。但是這樣漸漸的內(nèi)存中的東西就離我們?cè)絹?lái)越遠(yuǎn),直至現(xiàn)在很多入門(mén)前端的對(duì)內(nèi)存的情況一概不知,我也是,當(dāng)然這是錯(cuò)誤的。

內(nèi)存分配

內(nèi)存分配的最終目的就是為了配合垃圾回收機(jī)制,使得程序運(yùn)行時(shí)占用內(nèi)存更少,從而效率更高。

我們都知道數(shù)據(jù)類(lèi)型有兩種:基礎(chǔ)類(lèi)型和引用類(lèi)型,不同的類(lèi)型采用著不同的存儲(chǔ)方式。

基礎(chǔ)類(lèi)型與棧內(nèi)存

基礎(chǔ)類(lèi)型值包括:undefined、nullboolean、number、stringsymbol。這些類(lèi)型的值大多有固定的大小,JavaScript將它們保存在棧內(nèi)存中直接按值引用。
棧是一種線性的數(shù)據(jù)結(jié)構(gòu),典型特點(diǎn)是先進(jìn)后出, 后進(jìn)先出,當(dāng)JavaScript中一個(gè)方法執(zhí)行的時(shí)候,該方法就建立一個(gè)內(nèi)存棧,然后將方法中定義的變量放入棧中,當(dāng)我們需要的時(shí)候直接按值引用即可。當(dāng)方法執(zhí)行結(jié)束后就銷(xiāo)毀。

引用類(lèi)型與堆內(nèi)存

JavaScript的引用類(lèi)型大多長(zhǎng)度不固定,比如Array,它的長(zhǎng)度并不是固定的。他的值保存在堆內(nèi)存的對(duì)象中。然后將它的地址放入棧中,而且不允許我們直接訪問(wèn)堆內(nèi)存中的位置,所以我們操作的都是對(duì)象的引用,并不是實(shí)際的對(duì)象。
在程序中創(chuàng)建一個(gè)對(duì)象的成本是比較大的,在創(chuàng)建完成后就會(huì)被保存在堆數(shù)據(jù)區(qū),并不會(huì)隨著方法的結(jié)束而銷(xiāo)毀。只有當(dāng)這個(gè)對(duì)象沒(méi)有被任何引用變量引用的時(shí)候,垃圾回收的時(shí)候才會(huì)回收掉它。

垃圾回收

JavaScript具有自動(dòng)垃圾收集機(jī)制,執(zhí)行環(huán)境會(huì)負(fù)責(zé)找出那些不再繼續(xù)使用的變量然后釋放其占用的內(nèi)存。對(duì)于找出垃圾的方法通常有兩個(gè)策略:

標(biāo)記清除

這是最常用的垃圾收集機(jī)制。這種算法假定一個(gè)根對(duì)象,然后遍歷所有從根開(kāi)始引用的對(duì)象,垃圾收集器在運(yùn)行的時(shí)候會(huì)將他們加上標(biāo)記,然后去掉環(huán)境中使用的變量和被他們引用的變量的標(biāo)記。之后再被加上標(biāo)記的變量就是要?jiǎng)h除的,垃圾收集器將其釋放完成一次工作。

引用計(jì)數(shù)

這種機(jī)制為每一個(gè)值標(biāo)記被引用的次數(shù)并追蹤,當(dāng)被其他變量引用的時(shí)候就加一,反之就減一,當(dāng)引用次數(shù)變成 0 的時(shí)候就是需要回收的了。垃圾收集器下次運(yùn)行的時(shí)候就會(huì)把它釋放掉。但是這樣會(huì)有一個(gè)問(wèn)題,比如:

let obj1 = new Object();
let obj2 = new Object();

obj1.attr1 = obj2;
obj2.attr2 = obj1;

在這里obj1obj2各自互相引用,這塊語(yǔ)句執(zhí)行過(guò)后他們的引用次數(shù)永遠(yuǎn)不會(huì)變成 0 ,也就得不到回收,如果存在大量這種情況的話內(nèi)存就出問(wèn)題了。只要有出現(xiàn)循環(huán)引用的地方,這種機(jī)制就會(huì)出問(wèn)題。所以它無(wú)法處理循環(huán)引用的問(wèn)題。

V8引擎的垃圾回收

V8 采用一種叫做分代回收的策略。將內(nèi)存分為新生代和老生代,新生代存放存活時(shí)間段的對(duì)象,老生代則存放存活時(shí)間長(zhǎng)或者常駐內(nèi)存的對(duì)象。

  • 大多數(shù)的對(duì)象會(huì)被分配到新生代內(nèi)存中,回收算法將這里的內(nèi)存空間一分為二,一個(gè)處于使用狀態(tài)一個(gè)處于閑置狀態(tài)。分配對(duì)象的時(shí)候先把它放在使用區(qū)中,開(kāi)始垃圾回收的時(shí)候就檢查使用區(qū)中存活的對(duì)象,將他們復(fù)制到閑置區(qū)中并適當(dāng)緊縮,最后釋放使用區(qū)上剩下的數(shù)據(jù)。然后閑置區(qū)變成使用區(qū),使用區(qū)變成閑置區(qū),循環(huán)往復(fù)。

  • 當(dāng)一個(gè)對(duì)象經(jīng)過(guò)多次清理后依然存在,它就會(huì)被移動(dòng)到老生代,稱(chēng)為晉升。

  • 老生代占用內(nèi)存較多,主要采用標(biāo)記清除標(biāo)記整理兩個(gè)策略。在標(biāo)記階段遍歷堆中的所有對(duì)象,標(biāo)注那些活著的對(duì)象,然后在清除階段標(biāo)記清除會(huì)清除掉沒(méi)有被標(biāo)記的對(duì)象。但是這會(huì)產(chǎn)生內(nèi)存碎片。標(biāo)記清理在清理的時(shí)候可以解決碎片問(wèn)題,它將活著的對(duì)象向內(nèi)存中的一段移動(dòng),然后清理掉邊界外的內(nèi)存。不過(guò)這個(gè)過(guò)程涉及到數(shù)據(jù)移動(dòng),所以效率不是很高。

除此之外 V8 中還使用了增量標(biāo)記,讓垃圾回收與應(yīng)用邏輯交替進(jìn)行,以減少垃圾回收時(shí)的停頓時(shí)間;在標(biāo)記完成后還可以惰性清理;以及后期中引入了并行標(biāo)記和并行清理,通過(guò)并行來(lái)利用多核 CPU 性能。這些無(wú)疑讓 V8 成為了最出色的 JavaScript 引擎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容