您好,登錄后才能下訂單哦!
今天小編給大家分享一下V8的內存管理與垃圾回收算法是什么的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
V8最初為瀏覽器設計,遇到大內存使用的場景較少,在設計上默認對內存使用存在限制,只允許使用部分內存,64位系統可允許使用內存約1.4g,32位系統約0.7g。如下代碼所示,在Node中查看所依賴的V8引擎的內存限制方法:
process.memoryUsage(); // 返回內存的使用量,單位字節 { rss: 22953984, // 申請的總的堆內存 heapTotal: 9682944, // 已使用的堆內存 heapUsed: 5290344, external: 9388 }
V8
限制內存使用大小還有另一個重要原因,堆內存過大時V8
執行垃圾回收的時間較久(1.5g
要50ms
),做非增量式的垃圾回收要更久(1.5g
要1s
)。在后續講解了V8
的垃圾回收機制后相信大家更能感同身受。
雖然V8
引擎對內存使用做了限制,但是同樣暴露修改內存限制的方法,就是啟動V8
引擎時添加相關參數,下面代碼演示在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
環境修改新生代內存的語法:
node --v8-options | grep max
在引擎的垃圾自動回收機制的歷史演變中,人們發現是沒有一種通用的可以解決任何場景下垃圾回收的算法的。因此現代垃圾回收算法根據對象的存活時間將內存垃圾進行分代,分代垃圾回收算法就是對不同類別的內存垃圾實行不同的回收算法。
V8
將內存分為新生代
和老生代
兩種:
新生代內存中的對象存活時間較短
老生代內存中代對象存活時間較長或是常駐內存
新生代內存存放在新生代內存空間(semispace
)中,老生代內存存放在老生代內存空間中(oldspace
),如下圖所示:
新生代內存采用Scavenge
算法
老生代內存采用Mark-Sweep
和Mark-Compact
算法
下面我們看看Scavenge
的算法邏輯吧!
對于新生代內存的內存回收采用Scavenge
算法,Scavenge
的具體實現采用的是Cheney
算法。Cheney
算法是將新生代內存空間一分為二,一個空間處于使用狀態(FromSpace
),一個空間處于空閑狀態(稱為ToSpace
)。
在內存開始分配時,首先在FromSpace
中進行分配,垃圾回收機制執行時會檢查FromSpace
中的存活對象,存活對象會被會被復制到ToSpace
,非存活對象所占用的空間將被釋放,復制完成后FromSpace
和ToSpace
的角色將翻轉。當一個對象多次復制后依然處于存活狀態,則認為其是長期存活對象,此時將發生晉升,然后該對象被移動到老生代空間oldSpace
中,采用新的算法進行管理。
Scavenge
算法其實就是在兩個空間內來回復制存活對象,是典型的空間換時間做法,所以非常適合新生代內存,因為僅復制存活的對象且新生代內存中存活對象是占少數的。但是有如下幾個重要問題需要考慮:
引用避免重復拷貝
假設存在三個對象temp1、temp2、temp3
,其中temp2、temp3
都引用了temp1
,js代碼示例如下:
var temp2 = { ref: temp1, } var temp3 = { ref: temp1, } var temp1 = {}
從FromSpace
中拷貝temp2
到ToSpace
中時,發現引用了temp1
,便把temp1
也拷貝到ToSpace
,是一個遞歸的過程。但是在拷貝temp3
時發現也引用了temp1
,此時再把temp1
拷貝過去則重復了。
要避免重復拷貝,做法是拷貝時給對象添加一個標記visited
表示該節點已被訪問過,后續通過visited
屬性判斷是否拷貝對象。
拷貝后保持正確的引用關系
還是上述引用關系,由于temp1
不需要重復拷貝,temp3
被拷貝到ToSpace
之后不知道temp1
對象在ToSpace
中的內存地址。
做法是temp1
被拷貝過去后該對象節點上會生成新的field
屬性指向新的內存空間地址,同時更新到舊內存對象的forwarding
屬性上,因此temp3
就可以通過舊temp1
的forwarding
屬性找到在ToSpace
中的引用地址了。
內存對象同時存在于新生代和老生代之后,也帶來了問題:
內存對象跨代(跨空間)后如何標記
const temp1 = {} const temp2 = { ref: temp1, }
比如上述代碼中的兩個對象temp1
和temp2
都存在于新生代,其中temp2
引用了temp1
。假設在經過GC
之后temp2
晉升到了老生代,那么在下次GC
的標記階段,如何判斷temp1
是否是存活對象呢?
在基于可達性分析算法中要知道temp1
是否存活,就必須要知道是否有根對象引用
引用了temp1
對象。如此的話,年輕代的GC
就要遍歷所有的老生代對象判斷是否有根引用對象引用了temp1
對象,如此的話分代算法就沒有意義了。
解決版本就是維護一個記錄所有的跨代引用的記錄集,它是寫緩沖區
的一個列表。只要有老生代中的內存對象指向了新生代內存對象時,就將老生代中該對象的內存引用記錄到記錄集中。由于這種情況一般發生在對象寫的操作,顧稱此為寫屏障,還一種可能的情況就是發生在晉升時。記錄集的維護只要關心對象的寫操作和晉升操作即可。此是又帶來了另一個問題:
每次寫操作時維護記錄集的額外開銷
優化的手段是在一些Crankshaft
操作中是不需要寫屏障的,還有就是棧上內存對象的寫操作是不需要寫屏障的。還有一些,更多的手段就不在這里過多討論。
緩解Scavenge
算法內存利用率不高問題
新生代內存中存活對象占比是相對較小的,因此可以在分配空間時,ToSpace
可以分配的小一些。做法是將ToSpace
空間分成S0
和S1
兩部分,S0
用作于ToSpace
,S1
與原FromSpace
合并當成FromSpace
。
垃圾回收算法中,識別內存對象是否是垃圾的機制一般有兩種:引用計數和基于可達性分析。
基于可達性分析,就是找出所有的根引用(比如全局變量等),遍歷所有根引用,遞歸根引用上的所有引用,凡是被遍歷到的都是存活對象并打上標記,此時空間中的其他內存對象都是死對象,由此構建了一個有向圖。
考慮到遞歸的限制問題,遞歸邏輯一般采用非遞歸實現,常見的有廣度優先和深度優先算法。兩者的區別在于:
深度優先拷貝到ToSpace
時改變了內存對象的排列順序,使得有引用關系的對象距離較近。原因是拷貝完自己之后直接拷貝自己引用的對象,因此相關的對象便在ToSpace
中靠的較近
深度優先正好相反
因為CPU的緩存策略,會在讀取內存對象時有很大概率把他后面的對象一起讀,目的是為了更快的命中緩存。因為在代碼開發期間很常見的場景就是obj1.obj2.obj3
,此時CPU讀取obj1
時如果把后面的obj2
、obj3
一起讀的話,則很利于命中緩存。
所以深度優先的算法更利于業務邏輯命中緩存,但是其實現需要依賴額外的棧輔助實現算法,對內存空間有消耗。廣度優先則相反,無法提升緩存命中,但是其實現可以利用指針巧妙的避開空間消耗,算法的執行效率高。
新生代中的內存對象如果想晉升到老生代需要滿足如下幾個條件:
對象是否經歷過Scavenge
回收
ToSpace
的內存使用占比不能超過限制
判斷是否經歷過Scavenge
的GC的邏輯是,每次GC
時給存活對象的age
屬性+1
,當再次GC
的時候判斷age
屬性即可。基本的晉升示意圖如下所示:
老生代內存中,長期存活的對象較多,無法采取Scavenge
算法回收的原因在于:
存活對象較多導致復制效率低下
浪費了一半的內存空間
老生代內存空間的垃圾回收采用的是標記清除
(Mark-Sweep
)和標記整理
(Mark-Compact
)結合的方式。標記清除分為兩部分:
標記階段
清除階段(如果是標記整理則是整理階段)
在標記階段遍歷老生代堆內存中的所有內存對象,并對活著的對象做標記,清除階段只清理未被標記的對象。原因是:老生代內存中非存活對象占少數。
如上圖所示,標記清除存在的一個問題是清理之后存在了不連續的空間導致無法繼續利用,所以對于老生代內存空間的內存清理需要結合標記整理的方案。該方案是在標記過程中將活著的對象往一側移動,移動完成后再清理界外的所有非存活對象移除。
垃圾回收時需要暫停應用執行邏輯,待垃圾回收機制結束后再恢復應用執行邏輯,該行為稱為“全暫停”,也就是常說的Stop The World
,簡稱STW
。對新生代內存的垃圾回收該行為對應用執行影響不大,但是老生代內存由于存活對象較多,所以老生代內存的垃圾回收造成的全停頓影響非常大。
V8為了優化GC的全暫停時間,還引入了增量標記
、并發標記
、并行標記
、增量整理
、并行清理
、延遲清理
等方式。
衡量垃圾回收所用時間的一個重要指標是執行 GC
時主線程暫停的時間量。STW所帶來的影響是無法接受的,因此V8也采取的很多優化手段。
并行GC
GC的過程需要做大量的事情從而在主線程上導致STW現象,并行GC的做法是開多個輔助線程分擔GC的事情。該做法依然無法避免STW現象的,但是可以減少STW的總時間,取決于開啟的輔助線程數量。
增量GC
增量GC將GC工作進行拆分,并在主線程中間歇的分步執行。該做法并不會減少GC的時間,相反會稍微花銷,但是它同樣會減少GC的STW的總時間。
并發GC
并發GC是指GC在后臺運行,不再在主線程運行。該做法會避免STW現象。
空閑時間GC
Chrome
中動畫的渲染大約是60
幀(每幀約16ms
),如果當前渲染所花費時間每達到16.6ms
,此時則有空閑時間做其他事情,比如部分GC
任務。
想要提高執行效率要盡量減少垃圾回收的執行和消耗:
慎把內存當作緩存,小心把對象當作緩存,要合理限制過期時間和無限增長的問題,可以采用lru策略
Node
中避免使用內存存儲用戶會話,否則在內存中存放大量用戶會話對象導致老生代內存激增,影響清理性能進而影響應用執行性能和內存溢出。改進方式使用使用redis等。將緩存轉移到外部的好處:
減少常駐內存對象的數量,垃圾回收更高效
進程之間可以共享緩存
以上就是“V8的內存管理與垃圾回收算法是什么”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。