上文我們學(xué)習(xí)了js引擎垃圾回收機(jī)制,這篇文章,我們一起來看看V8引擎垃圾回收機(jī)制,看看V8在垃圾回收方面做了哪些優(yōu)化,有哪些方面性能的提升。v8渲染引擎講解,可以參考https://mp.weixin.qq.com/s/lOznk5GdDxr9rIRRg4MpaA
V8操作場景及存在限制
在一些實際應(yīng)用場景中,V8引擎實例的生命周期不會很長,V8這套內(nèi)存管理機(jī)制,在瀏覽器的應(yīng)用場景下綽綽有余,如果不幸發(fā)生內(nèi)存泄露等問題,僅僅會影響到某個終端用戶,不會對其他用戶造成影響。且無論這個V8實例占用了多少內(nèi)存,最終在關(guān)閉頁面時內(nèi)存都會被釋放,幾乎沒有太多管理的必要(當(dāng)然并不代表一些大型Web應(yīng)用不需要管理內(nèi)存)。但在node中,卻限制了開發(fā)者隨心所欲使用內(nèi)存的想法,一旦內(nèi)存發(fā)生泄漏,久而久之整個服務(wù)將會癱瘓(服務(wù)器不會頻繁的重啟)。要知曉V8為何限制內(nèi)存容量,則需要深入了解V8在內(nèi)存上使用的策略。只有這樣才能夠更好的進(jìn)行內(nèi)存管理。
在一般的后端開發(fā)語言中,在基本的內(nèi)存使用上沒有什么限制。然而node通過js使用內(nèi)存時就會發(fā)現(xiàn)只能使用部分內(nèi)存:64位操作系統(tǒng)默認(rèn)使用1.4G,32位操作系統(tǒng)默認(rèn)使用0.7G。
在了解垃圾回收算法前,我們先來了解一個概念--“全停頓”
垃圾回收算法在執(zhí)行前,需要將應(yīng)用邏輯暫停,執(zhí)行完垃圾回收后再執(zhí)行應(yīng)用邏輯,這種行為稱為 「全停頓」(Stop The World)。例如,如果一次GC需要50ms,應(yīng)用邏輯就會暫停50ms。為什么會暫停呢?一是因為js是單線程執(zhí)行的,進(jìn)入垃圾回收后,js應(yīng)用邏輯需要暫停,以留出空間給垃圾回收算法運(yùn)行。另一方面垃圾回收其實是非常耗時間的操作,比如:以 1.5GB 的垃圾回收堆內(nèi)存為例,V8 做一次小的垃圾回收需要50ms 以上,做一次非增量式(即將所有GC的數(shù)據(jù)統(tǒng)一處理,不分區(qū)/塊概念,一次執(zhí)行)的垃圾回收甚至要 1s 以上。
V8中的堆數(shù)據(jù)結(jié)構(gòu),可分為老生區(qū),新生區(qū),大對象區(qū),map區(qū)和代碼區(qū),
垃圾回收算法
本文主要針對新生區(qū)和老生區(qū)來對垃圾回收算法進(jìn)行解讀
1、Scavenge算法
所謂 Scavenge 算法,是把新生代空間對半劃分為兩個區(qū)域,新生區(qū)分為:一個Eden區(qū)(別名from區(qū))和兩個survivor區(qū)(別名to區(qū))(比例為8:1:1)
新的對象會首先被分配到 from區(qū),當(dāng)進(jìn)行垃圾回收的時候,會先將 from 區(qū)中 存活的對象復(fù)制到 to區(qū)進(jìn)行保存,對未存活的對象的空間進(jìn)行回收。
復(fù)制完成后, from區(qū)和 to區(qū)進(jìn)行調(diào)換,to區(qū)會變成新的 from區(qū),原來的from區(qū)則變成to區(qū)。這種算法稱之為 ”Scavenge“。
新生代內(nèi)存回收頻率很高,速度也很快,但是空間利用率很低,因為有一半的內(nèi)存空間處于"閑置"狀態(tài)。
老生代內(nèi)存回收
新生代中多次進(jìn)行回收仍然存活的對象會被轉(zhuǎn)移到空間較大的老生代內(nèi)存中,這種現(xiàn)象稱為晉升。以下兩種情況
在垃圾回收過程中,發(fā)現(xiàn)某個對象之前被清理過,那么將會晉升到老生代的內(nèi)存空間中
在 from 空間和 to 空間進(jìn)行反轉(zhuǎn)的過程中,如果 to 空間中的使用量已經(jīng)超過了 25% ,那么就將 from 中的對象直接晉升到老生代內(nèi)存空間中。
老生代占用內(nèi)存較多(64位為1.4GB,32位為700MB),老生代存活對象多,存活時間久,如果使用Scavenge算法,浪費(fèi)一半空間不說,復(fù)制如此大塊的內(nèi)存消耗時間將會相當(dāng)長。所以Scavenge算法顯然不適合。V8在老生代中的垃圾回收策略采用Mark-Sweep和Mark-Compact相結(jié)合
2、Mark-Sweep(標(biāo)記-清除)算法
老生代采用的是”標(biāo)記-清除“來回收未存活的對象。
分為標(biāo)記和清除兩個階段。標(biāo)記階段會遍歷堆中所有的對象,并對存活的對象進(jìn)行標(biāo)記,清除階段則是對未標(biāo)記的對象進(jìn)行清除。
3、標(biāo)記-整理(Mark-Compact)
標(biāo)記清除不會對內(nèi)存一分為二,所以不會浪費(fèi)空間。但是經(jīng)過標(biāo)記清除之后的內(nèi)存空間會生產(chǎn)很多不連續(xù)的碎片空間,這種不連續(xù)的碎片空間中,在遇到較大的對象時可能會由于空間不足而導(dǎo)致無法存儲。
為了解決內(nèi)存碎片的問題,需要使用另外一種算法 - 標(biāo)記-整理(Mark-Compact)。標(biāo)記整理對待未存活對象不是立即回收,而是將存活對象移動到一邊,然后直接清掉端邊界以外的內(nèi)存。
**4、增量標(biāo)記算法 --- **“全停頓”****
“全停頓” -- 參考上面解釋。為了避免垃圾回收時間過長影響其他程序的執(zhí)行,V8將標(biāo)記過程分成一個個小的子標(biāo)記過程,同時讓垃圾回收和JavaScript應(yīng)用邏輯代碼交替執(zhí)行,直到標(biāo)記階段完成。我們稱這個過程為增量標(biāo)記算法。
通俗理解,就是把垃圾回收這個大的任務(wù)分成一個個小任務(wù),穿插在 JavaScript任務(wù)中間執(zhí)行,這個過程其實跟 React Fiber 的設(shè)計思路類似。
5、惰性清理
由于標(biāo)記完成后,所有的對象都已經(jīng)被標(biāo)記,不是死對象就是活對象,堆上多少空間格局已經(jīng)確定。我們可以不必著急釋放那些死對象所占用的空間,而延遲清理過程的執(zhí)行。垃圾回收器可以根據(jù)需要逐一清理死對象所占用的內(nèi)存頁
<article style="margin: 0px 0px 1.5rem; padding: 0px; font-size: 17px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; box-sizing: border-box; line-height: 1.6; color: rgb(33, 37, 41); font-family: -apple-system, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; text-align: left; background-color: rgb(255, 255, 255);">
V8后續(xù)還引入了增量式整理(incremental compaction),以及并行標(biāo)記和并行清理,通過并行利用多核CPU來提升垃圾回收的性能
最后盜圖一張(@夏木),來總結(jié)V8的垃圾回收機(jī)制
老規(guī)矩,留一個思考題,下一篇文章給出參考答案。
如何編寫V8友好的高性能javascript代碼?歡迎小伙伴關(guān)注并留言
</article>