本文目錄
- 1.JS引擎的內(nèi)存
- 2.新生代和老生代的內(nèi)存回收
- 3.可達(dá)性
- 4.兩個(gè)引用的情況
- 5.相互關(guān)聯(lián)的對(duì)象
- 6.無(wú)法訪問(wèn)的數(shù)據(jù)塊
- 7.內(nèi)部算法
- 8.總結(jié)
1.JS引擎的內(nèi)存
JavaScript引擎的內(nèi)存空間主要分為棧和堆。
棧是臨時(shí)存儲(chǔ)空間,主要存儲(chǔ)局部變量和函數(shù)調(diào)用。
基本類型數(shù)據(jù)(Number, Boolean, String, Null, Undefined, Symbol, BigInt)保存在在棧內(nèi)存中。
引用類型數(shù)據(jù)保存在堆內(nèi)存中,引用數(shù)據(jù)類型的變量是一個(gè)指向堆內(nèi)存中實(shí)際對(duì)象的引用,存在棧中。
棧雖然很輕量,在使用時(shí)創(chuàng)建,使用結(jié)束后銷毀,但是不是可以無(wú)限增長(zhǎng)的,被分配的調(diào)用??臻g被占滿時(shí),就會(huì)引起”棧溢出“的錯(cuò)誤。
為什么基本數(shù)據(jù)類型存儲(chǔ)在棧中,引用數(shù)據(jù)類型存儲(chǔ)在堆中?
JavaScript引擎需要用棧來(lái)維護(hù)程序執(zhí)行期間的上下文的狀態(tài),如果??臻g大了的話,所有數(shù)據(jù)都存放在??臻g里面,會(huì)影響到上下文切換的效率,進(jìn)而影響整個(gè)程序的執(zhí)行效率。
堆空間存儲(chǔ)的數(shù)據(jù)比較復(fù)雜,大致可以劃分為下面 5 個(gè)區(qū)域:代碼區(qū)(Code Space)、Map 區(qū)(Map Space)、大對(duì)象區(qū)(Large Object Space)、新生代(New Space)、老生代(Old Space)。本篇文章主要討論新生代和老生代的內(nèi)存回收算法。
新生代內(nèi)存是臨時(shí)分配的內(nèi)存,存活時(shí)間段,老生代內(nèi)存是常駐內(nèi)存,存活時(shí)間長(zhǎng)。
2.新生代和老生代的內(nèi)存回收
新生代中用 Scavenge 算法來(lái)處理。所謂 Scavenge 算法,是把新生代空間對(duì)半劃分為兩個(gè)區(qū)域,一半是對(duì)象區(qū)域(from),一半是空閑區(qū)域 (to)。
新的對(duì)象會(huì)首先被分配到 from 空間,當(dāng)進(jìn)行垃圾回收的時(shí)候,會(huì)先將 from 空間中的 存活的對(duì)象復(fù)制到 to 空間進(jìn)行保存,對(duì)未存活的對(duì)象的空間進(jìn)行回收。
復(fù)制完成后, from 空間和 to 空間進(jìn)行調(diào)換,to 空間會(huì)變成新的 from 空間,原來(lái)的 from 空間則變成 to 空間。這種算法稱之為 ”Scavenge“。
新生代內(nèi)存回收頻率很高,速度也很快,但是空間利用率很低,因?yàn)橛幸话氲膬?nèi)存空間處于"閑置"狀態(tài)。
新生代中多次進(jìn)行回收仍然存活的對(duì)象會(huì)被轉(zhuǎn)移到空間較大的老生代內(nèi)存中,這種現(xiàn)象稱為晉升。
因?yàn)槔仙臻g較大,如果仍然用 Scavenge 算法來(lái)頻繁復(fù)制對(duì)象,那么性能開銷就太大了。
老生代采用的是”標(biāo)記清除“來(lái)回收未存活的對(duì)象。
分為標(biāo)記和清除兩個(gè)階段。標(biāo)記階段會(huì)遍歷堆中所有的對(duì)象,并對(duì)存活的對(duì)象進(jìn)行標(biāo)記,清除階段則是對(duì)未標(biāo)記的對(duì)象進(jìn)行清除。
標(biāo)記清除不會(huì)對(duì)內(nèi)存一分為二,所以不會(huì)浪費(fèi)空間。但是經(jīng)過(guò)標(biāo)記清除之后的內(nèi)存空間會(huì)生產(chǎn)很多不連續(xù)的碎片空間,這種不連續(xù)的碎片空間中,在遇到較大的對(duì)象時(shí)可能會(huì)由于空間不足而導(dǎo)致無(wú)法存儲(chǔ)。
為了解決內(nèi)存碎片的問(wèn)題,需要使用另外一種算法 - 標(biāo)記-整理(Mark-Compact)。標(biāo)記整理對(duì)待未存活對(duì)象不是立即回收,而是將存活對(duì)象移動(dòng)到一邊,然后直接清掉端邊界以外的內(nèi)存。
為了避免出現(xiàn)JavaScript應(yīng)用程序與垃圾回收器看到的不一致的情況,進(jìn)行垃圾回收的時(shí)候,都需要將正在運(yùn)行的程序停下來(lái),等待垃圾回收?qǐng)?zhí)行完成之后再回復(fù)程序的執(zhí)行,這種現(xiàn)象稱為“全停頓”。如果需要回收的數(shù)據(jù)過(guò)多,那么全停頓的時(shí)候就會(huì)比較長(zhǎng),會(huì)影響其他程序的正常執(zhí)行。
為了避免垃圾回收時(shí)間過(guò)長(zhǎng)影響其他程序的執(zhí)行,V8將標(biāo)記過(guò)程分成一個(gè)個(gè)小的子標(biāo)記過(guò)程,同時(shí)讓垃圾回收和JavaScript應(yīng)用邏輯代碼交替執(zhí)行,直到標(biāo)記階段完成。我們稱這個(gè)過(guò)程為增量標(biāo)記算法。
3.可達(dá)性
JavaScript 中內(nèi)存管理的主要概念是可達(dá)性。
簡(jiǎn)單地說(shuō),“可達(dá)性” 值就是那些以某種方式可訪問(wèn)或可用的值,它們被保證存儲(chǔ)在內(nèi)存中。
- 有一組基本的固有可達(dá)值,由于顯而易見(jiàn)的原因無(wú)法刪除。例如:
- 本地函數(shù)的局部變量和參數(shù)
- 當(dāng)前嵌套調(diào)用鏈上的其他函數(shù)的變量和參數(shù)
- 全局變量
- 還有一些其他的,內(nèi)部的
上面這些值這些值稱為根。
- 如果引用或引用鏈可以從根訪問(wèn)任何其他值,則認(rèn)為該值是可訪問(wèn)的。
例如,如果局部變量中有對(duì)象,并且該對(duì)象具有引用另一個(gè)對(duì)象的屬性,則該對(duì)象被視為可達(dá)性, 它引用的那些也是可以訪問(wèn)的。
JavaScript 引擎中有一個(gè)后臺(tái)進(jìn)程稱為垃圾回收器,它監(jiān)視所有對(duì)象,并刪除那些不可訪問(wèn)的對(duì)象。
一個(gè)簡(jiǎn)單的例子
下面是最簡(jiǎn)單的例子:
// user 具有對(duì)象的引用
let user = {
name: "John"
};

這里箭頭表示一個(gè)對(duì)象引用。全局變量“user”引用對(duì)象 {name:“John”} (為了簡(jiǎn)潔起見(jiàn),我們將其命名為John)。John 的 “name” 屬性存儲(chǔ)一個(gè)基本類型,因此它被繪制在對(duì)象中。
如果 user 的值被覆蓋,則引用丟失:
user = null;

現(xiàn)在 John 變成不可達(dá)的狀態(tài),沒(méi)有辦法訪問(wèn)它,沒(méi)有對(duì)它的引用。垃圾回收器將丟棄 John 數(shù)據(jù)并釋放內(nèi)存。
4.兩個(gè)引用的情況
現(xiàn)在讓我們假設(shè)我們將引用從 user 復(fù)制到 admin:
// user具有對(duì)象的引用
let user = {
name: "John"
};
let admin = user;

現(xiàn)在如果我們做同樣的事情:
user = null;
該對(duì)象仍然可以通過(guò) admin 全局變量訪問(wèn),所以它仍會(huì)在內(nèi)存中。如果我們也覆蓋admin,那么它會(huì)變得不可達(dá),然后被釋放。
5.相互關(guān)聯(lián)的對(duì)象
現(xiàn)在來(lái)看一個(gè)更復(fù)雜的例子, family 對(duì)象:
function marry (man, woman) {
woman.husban = man;
man.wife = woman;
return {
father: man,
mother: woman
}
}
let family = marry({
name: "John"
}, {
name: "Ann"
})
函數(shù) marry 通過(guò)給兩個(gè)對(duì)象彼此提供引用來(lái)“聯(lián)姻”它們,并返回一個(gè)包含兩個(gè)對(duì)象的新對(duì)象。
產(chǎn)生的內(nèi)存結(jié)構(gòu):

到目前為止,所有對(duì)象都是可訪問(wèn)的。
現(xiàn)在讓我們刪除兩個(gè)引用:
delete family.father;
delete family.mother.husband;

僅僅刪除這兩個(gè)引用中的一個(gè)是不夠的,因?yàn)樗袑?duì)象仍然是可訪問(wèn)的。
但是如果我們把這兩個(gè)都刪除,那么我們可以看到 John 不再有傳入的引用:

輸出引用無(wú)關(guān)緊要。只有傳入的對(duì)象才能使對(duì)象可訪問(wèn),因此,John 現(xiàn)在是不可訪問(wèn)的,并將從內(nèi)存中刪除所有不可訪問(wèn)的數(shù)據(jù)。
垃圾回收之后:

6.無(wú)法訪問(wèn)的數(shù)據(jù)塊
有可能整個(gè)相互連接的對(duì)象變得不可訪問(wèn)并從內(nèi)存中刪除。
源對(duì)象與上面的相同。然后:
family = null;
內(nèi)存中的圖片變成:

這個(gè)例子說(shuō)明了可達(dá)性的概念是多么重要。
很明顯,John和Ann仍然鏈接在一起,都有傳入的引用。但這還不夠。
“family”對(duì)象已經(jīng)從根上斷開了鏈接,不再有對(duì)它的引用,因此下面的整個(gè)塊變得不可到達(dá),并將被刪除。
7.內(nèi)部算法
基本的垃圾回收算法稱為“標(biāo)記-清除”,定期執(zhí)行以下“垃圾回收”步驟:
- 垃圾回收器獲取根并“標(biāo)記”(記住)它們。
- 然后它訪問(wèn)并“標(biāo)記”所有來(lái)自它們的引用。
- 然后它訪問(wèn)標(biāo)記的對(duì)象并標(biāo)記它們的引用。所有被訪問(wèn)的對(duì)象都被記住,以便以后不再訪問(wèn)同一個(gè)對(duì)象兩次。
- 以此類推,直到有未訪問(wèn)的引用(可以從根訪問(wèn))為止。
- 除標(biāo)記的對(duì)象外,所有對(duì)象都被刪除。
這就是垃圾收集的工作原理。JavaScript引擎應(yīng)用了許多優(yōu)化,使其運(yùn)行得更快,并且不影響執(zhí)行。
一些優(yōu)化:
- 分代回收——對(duì)象分為兩組:“新對(duì)象”和“舊對(duì)象”。許多對(duì)象出現(xiàn),完成它們的工作并迅速結(jié)束 ,它們很快就會(huì)被清理干凈。那些活得足夠久的對(duì)象,會(huì)變“老”,并且很少接受檢查。
- 增量回收——如果有很多對(duì)象,并且我們?cè)噲D一次遍歷并標(biāo)記整個(gè)對(duì)象集,那么可能會(huì)花費(fèi)一些時(shí)間,并在執(zhí)行中會(huì)有一定的延遲。因此,引擎試圖將垃圾回收分解為多個(gè)部分。然后,各個(gè)部分分別執(zhí)行。這需要額外的標(biāo)記來(lái)跟蹤變化,這樣有很多微小的延遲,而不是很大的延遲。
- 空閑時(shí)間收集——垃圾回收器只在 CPU 空閑時(shí)運(yùn)行,以減少對(duì)執(zhí)行的可能影響。
8.總結(jié)
1.什么是垃圾
一般來(lái)說(shuō)沒(méi)有被引用的對(duì)象就是垃圾,就是要被清除, 有個(gè)例外如果幾個(gè)對(duì)象引用形成一個(gè)環(huán),互相引用,但根訪問(wèn)不到它們,這幾個(gè)對(duì)象也是垃圾,也要被清除。
2.如何檢垃圾
常見(jiàn)的一種算法就是 標(biāo)記-清除 算法。