一、前言
在相當長一段時間里幽七,JS運行時的內(nèi)存問題都不被前端開發(fā)人員所關注景殷。
一方面,日常開發(fā)中基本不會遇上需要對內(nèi)存精準控制的場景澡屡,另一方面猿挚,寫JS不需要像寫 C/C++ 那樣在開發(fā)過程中隨時關注內(nèi)存的分配和釋放問題。
隨著生態(tài)的逐漸完善驶鹉,JS的執(zhí)行環(huán)境也不再局限于瀏覽器中绩蜻。目前,JS主要的執(zhí)行場景包括服務端(NodeJS室埋、Deno)办绝、桌面端(Electron)伊约、瀏覽器(Chrome、Microsoft Edge)孕蝉。
其中屡律,JS執(zhí)行引擎 V8 因其優(yōu)異的性能表現(xiàn),已經(jīng)成為主流昔驱。因此疹尾,本文對JS內(nèi)存管理模型的研究也將基于V8展開。
二骤肛、內(nèi)存結構
上圖展示了V8引擎的內(nèi)存結構纳本,整體上分成兩部分:
2.1 堆內(nèi)存
這里是存儲對象或動態(tài)數(shù)據(jù)的地方,也是占比最大的內(nèi)存區(qū)域腋颠。堆內(nèi)可以細分以下區(qū)域:
-
新生代(Young generation)
新生代是新對象存在的地方繁成,這些對象中的大多數(shù)都是短暫存在的。
這部分空間很惺缑怠(默認情況下16~32M
)巾腕,并且拆分成了兩個空間。
空間使用 Minor GC (Scavenger) 進行垃圾回收絮蒿。 -
老生代(Old generation)
新生代中經(jīng)歷了兩個Minor GC 周期的對象會被轉(zhuǎn)移到老生代中存放尊搬。這里占據(jù)著大量的內(nèi)存空間(默認情況下700~1400M
)
空間使用 Major GC(Mark-Sweep & Mark-Compact) 進行垃圾回收。 -
大對象區(qū)(LARGE OBJECT SPACE)
超過一定大小的對象會直接在大對象區(qū)中被創(chuàng)建土涝,并在不被使用時將其直接回收佛寿。 -
代碼區(qū)(CODE SPACE)
這是 即時編譯器(JIT) 存儲編譯代碼塊的地方。 -
其他區(qū)(CELL, PROPERTY CELL,MAP SPACE)
這些空間存放大小相同的對象但壮,并且對它們指向的對象類型有一些限制冀泻。
比如MAP SPACE
里存放的是hidden class信息,這能讓V8快速定位到對象值所在的內(nèi)存區(qū)蜡饵。
2.2 棧內(nèi)存
棧是用來存儲靜態(tài)數(shù)據(jù)的地方弹渔,內(nèi)容主要包括:
-
基本類型(Number, Boolean, String, Null, Undefined, Symbol, BigInt)
對于基本類型,系統(tǒng)會為新的變量在棧內(nèi)存中分配一個新值溯祸。 -
引用類型
系統(tǒng)會為新的變量在棧內(nèi)存中分配一個值肢专,這個值是一個對象的引用。 -
調(diào)用棧
解釋器創(chuàng)建了調(diào)用棧來記錄函數(shù)的調(diào)用過程焦辅。
每調(diào)用一個函數(shù)鸟召,解釋器就把該函數(shù)添加進調(diào)用棧,解釋器會為被添加進來的函數(shù)創(chuàng)建一個棧幀(用來保存函數(shù)的局部變量以及執(zhí)行語句)并立即執(zhí)行氨鹏。
如果正在執(zhí)行的函數(shù)還調(diào)用了其他函數(shù),新函數(shù)會繼續(xù)被添加進入調(diào)用棧压状。
三仆抵、內(nèi)存回收
棧內(nèi)存 其實是由操作系統(tǒng)進行自動管理的跟继,本文不做討論。
堆內(nèi)存 由V8進行管理镣丑,它占據(jù)最大的內(nèi)存空間舔糖,并且隨著程序運行時間的增加可能會持續(xù)增長,最終耗盡內(nèi)存莺匠。它也會變得碎片化金吗,影響程序運行的速度。這時內(nèi)存回收的重要性就體現(xiàn)出來了趣竣。
要進行內(nèi)存回收摇庙,需要先明確一個問題:什么樣的數(shù)據(jù)可以被回收。
V8通過回收 不可達對象 來釋放堆內(nèi)存遥缕,整個回收過程總體可以分為 標記 和 回收 兩個階段卫袒,涉及到的原理是 三色標記 和 分代回收 。
3.1 新生代
從 2.1堆內(nèi)存 小節(jié)中我們知道单匣,新生代被拆分成兩個小空間夕凝,使用 Minor GC (Scavenger) 進行垃圾回收。Minor GC (Scavenger) 我們可以簡稱為次要GC户秤。
兩個拆分出來的空間分別稱之為 to-space 和 from-space码秉。新加入的對象都會存放到from-space,當from-space被填滿時鸡号,會觸發(fā)次要GC转砖。
GC過程:
- 標記 從堆棧指針開始遞歸遍歷 from-space 中的對象圖查找 活躍對象。
-
復制 將這些對象復制到 to-space 中(包括被這些對象引用的所有對象)膜蠢。
重復此操作堪藐,直到掃描 from-space 中的所有對象。另外挑围,to-space 會分配連續(xù)的內(nèi)存塊礁竞,以減少碎片。 - 清除 清空 from-space杉辙,因為此時剩余的對象都是可回收的模捂。
- 交換 將 to-space 和 from-space 互換,即 to-space 變成 from-space蜘矢。
回收的最后一步是更新引用已移動的原始對象的指針狂男。每個被復制的對象都會留下一個新地址,用于更新原始指針以指向新位置品腹。
此時還存在一個問題:隨著活躍對象的累積岖食,from-space 很快會被填滿。
這時就輪到老生代出場了舞吭,在新生代中經(jīng)歷兩次GC并存活下來的對象泡垃,會被轉(zhuǎn)移到老生代析珊,這個過程被稱為晉升。如下圖:
至此蔑穴,新生代一次完整的垃圾回收就完成了忠寻。
3.2 老生代
在老生代中,垃圾回收為主要GC(Major GC)存和,包含了 標記清除(Mark-Sweep) 和 標記整理(Mark-Compact)奕剃。
GC過程:
- 標記 垃圾收集器識別哪些對象正在使用,哪些對象未使用捐腿。正在使用或可從GC根域遞歸訪問的對象被標記為活躍對象纵朋。
- 清除 清除未被標記為活躍對象的數(shù)據(jù)。
- 整理 如果碎片較多叙量,會將存活的對象移動到一起倡蝙,減少碎片提高內(nèi)存使用率。
3.3 三色標記
之前提到绞佩,垃圾回收時會先標記 活躍對象 來區(qū)分對象是否應該被回收寺鸥,這里就涉及到了三色標記算法。
標記位有三種顏色:
- 白色 對象未被標記品山。
- 灰色 對象已經(jīng)被標記胆建,但對象內(nèi)屬性還未遍歷完成。
- 黑色 對象已經(jīng)被標記肘交,且對象內(nèi)的屬性也已完成遍歷(活躍對象)笆载。
標記過程:
-
開始標記
初始所有對象都是白色,當收集器發(fā)現(xiàn)白色對象并將其推送到標記工作列表時涯呻,會將其標記成灰色凉驻。
-
標記完成對象
當收集器訪問目標對象的所有字段后,會將對象的顏色由灰色變?yōu)楹谏?br>
-
標記結束
當沒有灰色對象時复罐,代表標記結束涝登。此時剩余的白色對象表示無法訪問,可以被回收效诅。
經(jīng)歷過以上三步胀滚,一次完整的標記過程就完成了。
3.4 回收優(yōu)化
現(xiàn)在我們知道乱投,一次垃圾回收總需要經(jīng)歷 標記咽笼,回收,整理 等階段戚炫。
事實上剑刑,整個垃圾回收的過程是非常耗時的,比如光是標記整個堆內(nèi)存的活躍對象可能就要花費數(shù)百毫秒双肤,并且期間會阻塞程序的正常的執(zhí)行施掏。
[圖片上傳失敗...(image-ad8178-1632313567745)]
對此层宫,V8也在持續(xù)優(yōu)化,目前主要的優(yōu)化方式有:
增量標記
通過將標記任務拆分成一系列小任務其监,確保每次標記任務的持續(xù)時間在5~10
毫秒。
當堆的占用空間達到某個閾值大小時限匣,開始激活標記任務抖苦,此后每分配一定量的內(nèi)存,就會執(zhí)行增量標記米死。
增量標記與常規(guī)標記一樣锌历,本質(zhì)上都是深度優(yōu)先搜索,同樣使用的是三色標記算法峦筒。-
并行 & 并發(fā)
-
并行
V8會創(chuàng)建輔助線程究西,與主線程同時處理GC任務。這樣GC時間就約等于總時間除以協(xié)同的線程數(shù)了物喷。 -
并發(fā)
這里的并發(fā)是指主線程不再處理GC任務卤材,而是由輔助線程來執(zhí)行。這樣的好處是垃圾回收不再阻塞正常任務的執(zhí)行峦失。
-
惰性清除
當所有對象都被標記完成扇丛,此時已經(jīng)可以進行垃圾清除的工作。但實際上這部分工作可以延遲執(zhí)行尉辑,尤其是當內(nèi)存足夠的時候帆精。
V8會在合適的時間點執(zhí)行清除工作,比如工作線程空閑隧魄,或者內(nèi)存不足的時候卓练。
總結
內(nèi)存管理是一件非常復雜的事情,本文主要從 內(nèi)存結構 和 內(nèi)存回收 兩個方面進行了介紹购啄,并且隱藏了其中的一些細節(jié)襟企。
在V8的內(nèi)存管理模型中,其實能學習到一些通用的內(nèi)存管理思想和性能優(yōu)化方法闸溃。
比如整吆,垃圾回收總是會圍繞標記,清除辉川,整理展開表蝙。
在標記算法上,V8
和 JAVA
,Golang
,PHP
等編程語言一樣乓旗,使用了三色標記府蛇。
在性能優(yōu)化上,多進程/多線程屿愚, 分步汇跨, 異步务荆, 延遲 等方式也總能發(fā)揮作用。
參考資料
- 《深入淺出nodeJS》
- https://v8.dev/
- https://deepu.tech/memory-management-in-programming
- https://en.wikipedia.org/wiki/Tracing_garbage_collection#Implementation_strategies
- http://jayconrod.com/posts/55/a-tour-of-v8-garbage-collection
原文已在玩物得志技術公眾號上發(fā)布穷遂,鏈接:https://mp.weixin.qq.com/s/7mvP5jv5sBGNfnThap6JSQ