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