V8內(nèi)存管理及垃圾回收機(jī)制

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ì)象的引用,存在棧中。

基本類型賦值,系統(tǒng)會(huì)為新的變量在棧內(nèi)存中分配一個(gè)新值,這個(gè)很好理解。引用類型賦值,系統(tǒng)會(huì)為新的變量在棧內(nèi)存中分配一個(gè)值,這個(gè)值僅僅是指向同一個(gè)對(duì)象的引用,和原對(duì)象指向的都是堆內(nèi)存中的同一個(gè)對(duì)象。

對(duì)于函數(shù),解釋器創(chuàng)建了”調(diào)用?!皝?lái)記錄函數(shù)的調(diào)用過(guò)程。每調(diào)用一個(gè)函數(shù),解釋器就可以把該函數(shù)添加進(jìn)調(diào)用棧,解釋器會(huì)為被添加進(jìn)來(lái)的函數(shù)創(chuàng)建一個(gè)棧幀(用來(lái)保存函數(shù)的局部變量以及執(zhí)行語(yǔ)句)并立即執(zhí)行。如果正在執(zhí)行的函數(shù)還調(diào)用了其他函數(shù),新函數(shù)會(huì)繼續(xù)被添加進(jìn)入調(diào)用棧。函數(shù)執(zhí)行完成,對(duì)應(yīng)的棧幀立即被銷毀。

兩種查看調(diào)用棧的方法

  1. 使用 console.trace() 向Web控制臺(tái)輸出一個(gè)堆棧跟蹤.
  2. 瀏覽器開發(fā)者工具進(jìn)行斷點(diǎn)調(diào)試

棧雖然很輕量,在使用時(shí)創(chuàng)建,使用結(jié)束后銷毀,但是不是可以無(wú)限增長(zhǎng)的,被分配的調(diào)用棧空間被占滿時(shí),就會(huì)引起”棧溢出“的錯(cuò)誤。

(function foo() {
    foo()
})()
Maximum call stack size exceeded

為什么基本數(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)。

新生代內(nèi)存和老生代內(nèi)存

新生代內(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“。

Scavenge 算法

新生代內(nèi)存回收頻率很高,速度也很快,但是空間利用率很低,因?yàn)橛幸话氲膬?nèi)存空間處于"閑置"狀態(tài)。

老生代內(nèi)存回收

新生代中多次進(jìn)行回收仍然存活的對(duì)象會(huì)被轉(zhuǎn)移到空間較大的老生代內(nèi)存中,這種現(xiàn)象稱為晉升。以下兩種情況

  1. 在垃圾回收過(guò)程中,發(fā)現(xiàn)某個(gè)對(duì)象之前被清理過(guò),那么將會(huì)晉升到老生代的內(nèi)存空間中
  2. 在 from 空間和 to 空間進(jìn)行反轉(zhuǎn)的過(guò)程中,如果 to 空間中的使用量已經(jīng)超過(guò)了 25% ,那么就講 from 中的對(duì)象直接晉升到老生代內(nèi)存空間中。

因?yàn)槔仙臻g較大,如果仍然用 Scavenge 算法來(lái)頻繁復(fù)制對(duì)象,那么性能開銷就太大了。

標(biāo)記-清除(Mark-Sweep)

老生代采用的是”標(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)記清除過(guò)程

標(biāo)記-整理(Mark-Compact)

標(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)存。

標(biāo)記整理過(guò)程

增量標(biāo)記

為了避免出現(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í)行。


垃圾回收過(guò)程

為了避免垃圾回收時(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)記算法。

增量標(biāo)記

通俗理解,就是把垃圾回收這個(gè)大的任務(wù)分成一個(gè)個(gè)小任務(wù),穿插在 JavaScript任務(wù)中間執(zhí)行,這個(gè)過(guò)程其實(shí)跟 React Fiber 的設(shè)計(jì)思路類似。

參考

其他

最近發(fā)起了一個(gè)100天前端進(jìn)階計(jì)劃,主要是深挖每個(gè)知識(shí)點(diǎn)背后的原理,歡迎關(guān)注 微信公眾號(hào)「牧碼的星星」,我們一起學(xué)習(xí),打卡100天。同時(shí)也會(huì)分享一些自己學(xué)習(xí)的一些心得和想法,歡迎大家一起交流。

image
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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