本篇文章帶大家了解一下V8引擎的內(nèi)存管理與垃圾回收算法,希望對(duì)大家有所幫助!
眾所周知,JS是自動(dòng)管理垃圾回收的,開(kāi)發(fā)者不需要關(guān)心內(nèi)存的分配與回收。而且垃圾回收機(jī)制在前端面試中也是??嫉牟糠?。本文主要講解V8的分代垃圾回收算法,希望閱讀本文后的小伙伴能夠?qū)?code>V8垃圾回收機(jī)制有個(gè)痛徹
(哈哈,是痛徹
!?。。┑牧私?,文章主要涵蓋如下內(nèi)容:
V8
的內(nèi)存限制與解決辦法- 新生代內(nèi)存對(duì)象的
Scavenge
算法 - 基于
可達(dá)性分析算法
標(biāo)記存活對(duì)象的邏輯以及優(yōu)化手段 - 新生代內(nèi)存對(duì)象的晉升條件、
Scavenge
算法的深度/廣度優(yōu)先區(qū)別- 跨代內(nèi)存的的寫(xiě)屏障
- 老生代內(nèi)存對(duì)象的標(biāo)記清除/整理算法
GC
的STW
原因及優(yōu)化策略
V8的內(nèi)存限制與解決辦法
V8最初為瀏覽器設(shè)計(jì),遇到大內(nèi)存使用的場(chǎng)景較少,在設(shè)計(jì)上默認(rèn)對(duì)內(nèi)存使用存在限制,只允許使用部分內(nèi)存,64位系統(tǒng)可允許使用內(nèi)存約1.4g,32位系統(tǒng)約0.7g。如下代碼所示,在Node中查看所依賴的V8引擎的內(nèi)存限制方法:
process.memoryUsage(); // 返回內(nèi)存的使用量,單位字節(jié) { rss: 22953984, // 申請(qǐng)的總的堆內(nèi)存 heapTotal: 9682944, // 已使用的堆內(nèi)存 heapUsed: 5290344, external: 9388 }
V8
限制內(nèi)存使用大小還有另一個(gè)重要原因,堆內(nèi)存過(guò)大時(shí)V8
執(zhí)行垃圾回收的時(shí)間較久(1.5g
要50ms
),做非增量式的垃圾回收要更久(1.5g
要1s
)。在后續(xù)講解了V8
的垃圾回收機(jī)制后相信大家更能感同身受。
雖然V8
引擎對(duì)內(nèi)存使用做了限制,但是同樣暴露修改內(nèi)存限制的方法,就是啟動(dòng)V8
引擎時(shí)添加相關(guān)參數(shù),下面代碼演示在Node
中修改依賴的V8
引擎內(nèi)存限制:
# 更改老生代的內(nèi)存限制,單位mb node --max-old-space-size=2048 index.js # 更改新生代的內(nèi)存限制,單位mb node --max-semi-space-size=1024=64 index.js
這里需要注意的是更改的新生代的內(nèi)存的語(yǔ)法已經(jīng)更改為上述的寫(xiě)法,且單位也由kb
變成了mb
,舊的寫(xiě)法是node --max-new-space-size
,可以通過(guò)下面命令查詢當(dāng)前Node
環(huán)境修改新生代內(nèi)存的語(yǔ)法:
node --v8-options | grep max
V8垃圾回收策略
在引擎的垃圾自動(dòng)回收機(jī)制的歷史演變中,人們發(fā)現(xiàn)是沒(méi)有一種通用的可以解決任何場(chǎng)景下垃圾回收的算法的。因此現(xiàn)代垃圾回收算法根據(jù)對(duì)象的存活時(shí)間將內(nèi)存垃圾進(jìn)行分代,分代垃圾回收算法就是對(duì)不同類(lèi)別的內(nèi)存垃圾實(shí)行不同的回收算法。
V8
將內(nèi)存分為新生代
和老生代
兩種:
- 新生代內(nèi)存中的對(duì)象存活時(shí)間較短
- 老生代內(nèi)存中代對(duì)象存活時(shí)間較長(zhǎng)或是常駐內(nèi)存
新生代內(nèi)存存放在新生代內(nèi)存空間(semispace
)中,老生代內(nèi)存存放在老生代內(nèi)存空間中(oldspace
),如下圖所示:
- 新生代內(nèi)存采用
Scavenge
算法 - 老生代內(nèi)存采用
Mark-Sweep
和Mark-Compact
算法
下面我們看看Scavenge
的算法邏輯吧!
Scavenge算法
對(duì)于新生代內(nèi)存的內(nèi)存回收采用Scavenge
算法,Scavenge
的具體實(shí)現(xiàn)采用的是Cheney
算法。Cheney
算法是將新生代內(nèi)存空間一分為二,一個(gè)空間處于使用狀態(tài)(FromSpace
),一個(gè)空間處于空閑狀態(tài)(稱(chēng)為ToSpace
)。
在內(nèi)存開(kāi)始分配時(shí),首先在FromSpace
中進(jìn)行分配,垃圾回收機(jī)制執(zhí)行時(shí)會(huì)檢查FromSpace
中的存活對(duì)象,存活對(duì)象會(huì)被會(huì)被復(fù)制到ToSpace
,非存活對(duì)象所占用的空間將被釋放,復(fù)制完成后FromSpace
和ToSpace
的角色將翻轉(zhuǎn)。當(dāng)一個(gè)對(duì)象多次復(fù)制后依然處于存活狀態(tài),則認(rèn)為其是長(zhǎng)期存活對(duì)象,此時(shí)將發(fā)生晉升,然后該對(duì)象被移動(dòng)到老生代空間oldSpace
中,采用新的算法進(jìn)行管理。
Scavenge
算法其實(shí)就是在兩個(gè)空間內(nèi)來(lái)回復(fù)制存活對(duì)象,是典型的空間換時(shí)間做法,所以非常適合新生代內(nèi)存,因?yàn)閮H復(fù)制存活的對(duì)象且新生代內(nèi)存中存活對(duì)象是占少數(shù)的。但是有如下幾個(gè)重要問(wèn)題需要考慮:
- 引用避免重復(fù)拷貝
假設(shè)存在三個(gè)對(duì)象temp1、temp2、temp3
,其中temp2、temp3
都引用了temp1
,js代碼示例如下:
var temp2 = { ref: temp1, } var temp3 = { ref: temp1, } var temp1 = {}
從FromSpace
中拷貝temp2
到ToSpace
中時(shí),發(fā)現(xiàn)引用了temp1
,便把temp1
也拷貝到ToSpace
,是一個(gè)遞歸的過(guò)程。但是在拷貝temp3
時(shí)發(fā)現(xiàn)也引用了temp1
,此時(shí)再把temp1
拷貝過(guò)去則重復(fù)了。
要避免重復(fù)拷貝,做法是拷貝時(shí)給對(duì)象添加一個(gè)標(biāo)記visited
表示該節(jié)點(diǎn)已被訪問(wèn)過(guò),后續(xù)通過(guò)visited
屬性判斷是否拷貝對(duì)象。
- 拷貝后保持正確的引用關(guān)系
還是上述引用關(guān)系,由于temp1
不需要重復(fù)拷貝,temp3
被拷貝到ToSpace
之后不知道temp1
對(duì)象在ToSpace
中的內(nèi)存地址。
做法是temp1
被拷貝過(guò)去后該對(duì)象節(jié)點(diǎn)上會(huì)生成新的field
屬性指向新的內(nèi)存空間地址,同時(shí)更新到舊內(nèi)存對(duì)象的forwarding
屬性上,因此temp3
就可以通過(guò)舊temp1
的forwarding
屬性找到在ToSpace
中的引用地址了。
內(nèi)存對(duì)象同時(shí)存在于新生代和老生代之后,也帶來(lái)了問(wèn)題:
- 內(nèi)存對(duì)象跨代(跨空間)后如何標(biāo)記
const temp1 = {} const temp2 = { ref: temp1, }
比如上述代碼中的兩個(gè)對(duì)象temp1
和temp2
都存在于新生代,其中temp2
引用了temp1
。假設(shè)在經(jīng)過(guò)GC
之后temp2
晉升到了老生代,那么在下次GC
的標(biāo)記階段,如何判斷temp1
是否是存活對(duì)象呢?
在基于可達(dá)性分析算法中要知道temp1
是否存活,就必須要知道是否有根對(duì)象引用
引用了temp1
對(duì)象。如此的話,年輕代的GC
就要遍歷所有的老生代對(duì)象判斷是否有根引用對(duì)象引用了temp1
對(duì)象,如此的話分代算法就沒(méi)有意義了。
解決版本就是維護(hù)一個(gè)記錄所有的跨代引用的記錄集,它是寫(xiě)緩沖區(qū)
的一個(gè)列表。只要有老生代中的內(nèi)存對(duì)象指向了新生代內(nèi)存對(duì)象時(shí),就將老生代中該對(duì)象的內(nèi)存引用記錄到記錄集中。由于這種情況一般發(fā)生在對(duì)象寫(xiě)的操作,顧稱(chēng)此為寫(xiě)屏障,還一種可能的情況就是發(fā)生在晉升時(shí)。記錄集的維護(hù)只要關(guān)心對(duì)象的寫(xiě)操作和晉升操作即可。此是又帶來(lái)了另一個(gè)問(wèn)題:
- 每次寫(xiě)操作時(shí)維護(hù)記錄集的額外開(kāi)銷(xiāo)
優(yōu)化的手段是在一些Crankshaft
操作中是不需要寫(xiě)屏障的,還有就是棧上內(nèi)存對(duì)象的寫(xiě)操作是不需要寫(xiě)屏障的。還有一些,