V8 引擎是如何給 JS"打掃房間"的 ?
當(dāng)前位置:點(diǎn)晴教程→知識(shí)管理交流
→『 技術(shù)文檔交流 』
JS 語(yǔ)言不像 C/C++, 讓程序員自己去開(kāi)辟或者釋放內(nèi)存,而是類(lèi)似Java,采用自己的一套垃圾回收算法進(jìn)行自動(dòng)的內(nèi)存管理。今天就從內(nèi)存結(jié)構(gòu)說(shuō)起,一步步聊聊 V8 的垃圾回收機(jī)制。 先搞懂JS 的內(nèi)存都存在哪里?JS 的內(nèi)存存儲(chǔ)分兩塊:棧(Stack) 和堆(Heap) ,就像家里的 "鞋柜" 和 "儲(chǔ)物間"—— 常用的小東西放鞋柜,大件雜物放儲(chǔ)物間。 棧棧是一塊連續(xù)的內(nèi)存空間,就像排隊(duì)的抽屜,每個(gè)抽屜大小固定。它主要存兩種東西:
棧的回收特別簡(jiǎn)單:函數(shù)執(zhí)行時(shí)會(huì)創(chuàng)建 " 堆堆是一塊不連續(xù)的內(nèi)存空間,大小不固定,就像開(kāi)放式儲(chǔ)物間,專(zhuān)門(mén)存引用類(lèi)型(對(duì)象、數(shù)組、函數(shù)等)。比如創(chuàng)建一個(gè) 堆的麻煩在于:對(duì)象不會(huì)像棧里的變量那樣 "用完就走"。比如全局對(duì)象 V8 的內(nèi)存結(jié)構(gòu)V8 引擎把堆內(nèi)存又細(xì)分了兩塊:
這就是 V8 的 "分代回收" 思路:不同生命周期的對(duì)象,用不同的方式回收,效率更高。 棧內(nèi)存的回收棧的回收幾乎不用我們操心,全靠 JS 引擎的 "執(zhí)行上下文管理"。舉個(gè)例子:
這種回收方式效率極高,幾乎不消耗額外性能,所以棧內(nèi)存很少出問(wèn)題。 新生代內(nèi)存的回收新生代存的都是 "短命對(duì)象",比如循環(huán)里創(chuàng)建的臨時(shí)對(duì)象:
這些對(duì)象的回收,V8 用的是Scavenge 算法,核心是 "復(fù)制存活對(duì)象,清空剩余空間"。具體步驟可以腦補(bǔ)成這樣:
為什么要這么折騰?主要是為了避免 " 而復(fù)制到 To 空間時(shí)按順序排列,存活對(duì)象會(huì)擠在一起,剩下的空間是一整塊連續(xù)區(qū)域,下次分配新對(duì)象就很方便。 當(dāng)然,這種方式也有代價(jià):新生代內(nèi)存實(shí)際只能用一半(總有一個(gè)空間閑置)。但好在新生代對(duì)象存活時(shí)間短,復(fù)制成本低,總體算下來(lái)比標(biāo)記清除快得多。 老生代內(nèi)存的回收當(dāng)新生代的對(duì)象 "活過(guò)" 多次回收(比如被全局變量引用,或者在閉包里被長(zhǎng)期持有),就會(huì)被 "
老生代的對(duì)象要么體積大,要么存活久,用 Scavenge 算法復(fù)制太費(fèi)時(shí)間,所以 V8 換了套思路:標(biāo)記 - 清除+標(biāo)記 - 整理。 第一步:標(biāo)記 - 清除
這種方式解決了引用計(jì)數(shù)法的 "循環(huán)引用" 問(wèn)題。比如兩個(gè)對(duì)象互相引用,但都不再被全局訪(fǎng)問(wèn),標(biāo)記階段它們不會(huì)被標(biāo)記,清除階段會(huì)被回收 —— 而引用計(jì)數(shù)法會(huì)因?yàn)樗鼈兓ハ嘁?,?jì)數(shù)不為 0,永遠(yuǎn)不回收,導(dǎo)致內(nèi)存泄漏。 第二步:標(biāo)記 - 整理標(biāo)記 - 清除后,堆內(nèi)存會(huì)像被挖過(guò)的地一樣坑坑洼洼:存活對(duì)象零散分布,中間夾雜著被回收的空白區(qū)域(內(nèi)存碎片)。下次想分配一個(gè)大對(duì)象,可能找不到連續(xù)的空間,明明總內(nèi)存夠,卻分配失敗。 所以 V8 會(huì)緊接著做 "標(biāo)記 - 整理":把所有存活對(duì)象往內(nèi)存的一端 "擠",讓空白區(qū)域集中到另一端,形成一整塊連續(xù)的空閑內(nèi)存。就像把衣柜里的衣服都推到左邊,右邊留出一大塊空地放新衣服。 老生代的"增量標(biāo)記"JS 是單線(xiàn)程的,一旦開(kāi)始垃圾回收,JS 代碼就會(huì)暫停(稱(chēng)為 "Stop-The-World")。如果老生代內(nèi)存很大,一次完整的標(biāo)記 - 清除可能要卡 1 秒以上 —— 用戶(hù)點(diǎn)按鈕沒(méi)反應(yīng),頁(yè)面像死機(jī)了一樣。 為了解決這個(gè)問(wèn)題,V8 用了增量標(biāo)記:把原本一口氣完成的標(biāo)記階段,拆成一小塊一小塊,穿插在 JS 代碼執(zhí)行間隙。比如標(biāo)記 10ms,就讓 JS 執(zhí)行 20ms,再標(biāo)記 10ms... ,如果循環(huán),直到標(biāo)記階段完成才進(jìn)入內(nèi)存碎片的整理上面來(lái),不耽誤JS代碼的正常執(zhí)行。 這樣一來(lái),單次垃圾回收的阻塞時(shí)間從幾百毫秒降到幾十毫秒,用戶(hù)幾乎感覺(jué)不到卡頓。數(shù)據(jù)顯示,增量標(biāo)記能把垃圾回收的阻塞時(shí)間減少到原來(lái)的 1/6,對(duì)大型應(yīng)用來(lái)說(shuō)太重要了。 最后搞懂 V8 的回收機(jī)制后,我總結(jié)了幾個(gè)對(duì)開(kāi)發(fā)有用的點(diǎn):
?轉(zhuǎn)自https://juejin.cn/post/7524812060761489418 該文章在 2025/7/10 9:53:21 編輯過(guò) |
相關(guān)文章
正在查詢(xún)... |