溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

V8的內(nèi)存管理與垃圾回收算法是什么

發(fā)布時間:2022-04-28 10:15:39 來源:億速云 閱讀:110 作者:zzz 欄目:web開發(fā)

今天小編給大家分享一下V8的內(nèi)存管理與垃圾回收算法是什么的相關(guān)知識點,內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

V8的內(nèi)存管理與垃圾回收算法是什么

V8的內(nèi)存限制與解決辦法

V8最初為瀏覽器設(shè)計,遇到大內(nèi)存使用的場景較少,在設(shè)計上默認(rèn)對內(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,
  // 申請的總的堆內(nèi)存
  heapTotal: 9682944,
  // 已使用的堆內(nèi)存
  heapUsed: 5290344,
  external: 9388
}

V8的內(nèi)存管理與垃圾回收算法是什么

V8限制內(nèi)存使用大小還有另一個重要原因,堆內(nèi)存過大時V8執(zhí)行垃圾回收的時間較久(1.5g50ms),做非增量式的垃圾回收要更久(1.5g1s)。在后續(xù)講解了V8的垃圾回收機制后相信大家更能感同身受。

雖然V8引擎對內(nèi)存使用做了限制,但是同樣暴露修改內(nèi)存限制的方法,就是啟動V8引擎時添加相關(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)存的語法已經(jīng)更改為上述的寫法,且單位也由kb變成了mb,舊的寫法是node --max-new-space-size,可以通過下面命令查詢當(dāng)前Node環(huán)境修改新生代內(nèi)存的語法:

node --v8-options | grep max

V8的內(nèi)存管理與垃圾回收算法是什么

V8垃圾回收策略

在引擎的垃圾自動回收機制的歷史演變中,人們發(fā)現(xiàn)是沒有一種通用的可以解決任何場景下垃圾回收的算法的。因此現(xiàn)代垃圾回收算法根據(jù)對象的存活時間將內(nèi)存垃圾進行分代,分代垃圾回收算法就是對不同類別的內(nèi)存垃圾實行不同的回收算法。

V8將內(nèi)存分為新生代老生代兩種:

  • 新生代內(nèi)存中的對象存活時間較短

  • 老生代內(nèi)存中代對象存活時間較長或是常駐內(nèi)存

新生代內(nèi)存存放在新生代內(nèi)存空間(semispace)中,老生代內(nèi)存存放在老生代內(nèi)存空間中(oldspace),如下圖所示:

V8的內(nèi)存管理與垃圾回收算法是什么

  • 新生代內(nèi)存采用Scavenge算法

  • 老生代內(nèi)存采用Mark-SweepMark-Compact算法

下面我們看看Scavenge的算法邏輯吧!

Scavenge算法

對于新生代內(nèi)存的內(nèi)存回收采用Scavenge算法,Scavenge的具體實現(xiàn)采用的是Cheney算法。Cheney算法是將新生代內(nèi)存空間一分為二,一個空間處于使用狀態(tài)(FromSpace),一個空間處于空閑狀態(tài)(稱為ToSpace)。

V8的內(nèi)存管理與垃圾回收算法是什么

在內(nèi)存開始分配時,首先在FromSpace中進行分配,垃圾回收機制執(zhí)行時會檢查FromSpace中的存活對象,存活對象會被會被復(fù)制到ToSpace,非存活對象所占用的空間將被釋放,復(fù)制完成后FromSpaceToSpace的角色將翻轉(zhuǎn)。當(dāng)一個對象多次復(fù)制后依然處于存活狀態(tài),則認(rèn)為其是長期存活對象,此時將發(fā)生晉升,然后該對象被移動到老生代空間oldSpace中,采用新的算法進行管理。

V8的內(nèi)存管理與垃圾回收算法是什么

Scavenge算法其實就是在兩個空間內(nèi)來回復(fù)制存活對象,是典型的空間換時間做法,所以非常適合新生代內(nèi)存,因為僅復(fù)制存活的對象且新生代內(nèi)存中存活對象是占少數(shù)的。但是有如下幾個重要問題需要考慮:

  • 引用避免重復(fù)拷貝

假設(shè)存在三個對象temp1、temp2、temp3,其中temp2、temp3都引用了temp1,js代碼示例如下:

var temp2 = {
  ref: temp1,
}

var temp3 = {
  ref: temp1,
}

var temp1 = {}

FromSpace中拷貝temp2ToSpace中時,發(fā)現(xiàn)引用了temp1,便把temp1也拷貝到ToSpace,是一個遞歸的過程。但是在拷貝temp3時發(fā)現(xiàn)也引用了temp1,此時再把temp1拷貝過去則重復(fù)了。

要避免重復(fù)拷貝,做法是拷貝時給對象添加一個標(biāo)記visited表示該節(jié)點已被訪問過,后續(xù)通過visited屬性判斷是否拷貝對象。

  • 拷貝后保持正確的引用關(guān)系

還是上述引用關(guān)系,由于temp1不需要重復(fù)拷貝,temp3被拷貝到ToSpace之后不知道temp1對象在ToSpace中的內(nèi)存地址。

做法是temp1被拷貝過去后該對象節(jié)點上會生成新的field屬性指向新的內(nèi)存空間地址,同時更新到舊內(nèi)存對象的forwarding屬性上,因此temp3就可以通過舊temp1forwarding屬性找到在ToSpace中的引用地址了。

內(nèi)存對象同時存在于新生代和老生代之后,也帶來了問題:

  • 內(nèi)存對象跨代(跨空間)后如何標(biāo)記

const temp1 = {}

const temp2 = {
  ref: temp1,
}

比如上述代碼中的兩個對象temp1temp2都存在于新生代,其中temp2引用了temp1。假設(shè)在經(jīng)過GC之后temp2晉升到了老生代,那么在下次GC的標(biāo)記階段,如何判斷temp1是否是存活對象呢?

在基于可達性分析算法中要知道temp1是否存活,就必須要知道是否有根對象引用引用了temp1對象。如此的話,年輕代的GC就要遍歷所有的老生代對象判斷是否有根引用對象引用了temp1對象,如此的話分代算法就沒有意義了。

解決版本就是維護一個記錄所有的跨代引用的記錄集,它是寫緩沖區(qū)的一個列表。只要有老生代中的內(nèi)存對象指向了新生代內(nèi)存對象時,就將老生代中該對象的內(nèi)存引用記錄到記錄集中。由于這種情況一般發(fā)生在對象寫的操作,顧稱此為寫屏障,還一種可能的情況就是發(fā)生在晉升時。記錄集的維護只要關(guān)心對象的寫操作和晉升操作即可。此是又帶來了另一個問題:

  • 每次寫操作時維護記錄集的額外開銷

優(yōu)化的手段是在一些Crankshaft操作中是不需要寫屏障的,還有就是棧上內(nèi)存對象的寫操作是不需要寫屏障的。還有一些,更多的手段就不在這里過多討論。

  • 緩解Scavenge算法內(nèi)存利用率不高問題

新生代內(nèi)存中存活對象占比是相對較小的,因此可以在分配空間時,ToSpace可以分配的小一些。做法是將ToSpace空間分成S0S1兩部分,S0用作于ToSpaceS1與原FromSpace合并當(dāng)成FromSpace。

V8的內(nèi)存管理與垃圾回收算法是什么

Scavenge算法中深度/廣度優(yōu)先的區(qū)別

垃圾回收算法中,識別內(nèi)存對象是否是垃圾的機制一般有兩種:引用計數(shù)基于可達性分析

基于可達性分析,就是找出所有的根引用(比如全局變量等),遍歷所有根引用,遞歸根引用上的所有引用,凡是被遍歷到的都是存活對象并打上標(biāo)記,此時空間中的其他內(nèi)存對象都是死對象,由此構(gòu)建了一個有向圖。

考慮到遞歸的限制問題,遞歸邏輯一般采用非遞歸實現(xiàn),常見的有廣度優(yōu)先和深度優(yōu)先算法。兩者的區(qū)別在于:

  • 深度優(yōu)先拷貝到ToSpace時改變了內(nèi)存對象的排列順序,使得有引用關(guān)系的對象距離較近。原因是拷貝完自己之后直接拷貝自己引用的對象,因此相關(guān)的對象便在ToSpace中靠的較近

  • 深度優(yōu)先正好相反

因為CPU的緩存策略,會在讀取內(nèi)存對象時有很大概率把他后面的對象一起讀,目的是為了更快的命中緩存。因為在代碼開發(fā)期間很常見的場景就是obj1.obj2.obj3,此時CPU讀取obj1時如果把后面的obj2、obj3一起讀的話,則很利于命中緩存。

所以深度優(yōu)先的算法更利于業(yè)務(wù)邏輯命中緩存,但是其實現(xiàn)需要依賴額外的棧輔助實現(xiàn)算法,對內(nèi)存空間有消耗。廣度優(yōu)先則相反,無法提升緩存命中,但是其實現(xiàn)可以利用指針巧妙的避開空間消耗,算法的執(zhí)行效率高。

新生代內(nèi)存對象的晉升條件

新生代中的內(nèi)存對象如果想晉升到老生代需要滿足如下幾個條件:

  • 對象是否經(jīng)歷過Scavenge回收

  • ToSpace的內(nèi)存使用占比不能超過限制

判斷是否經(jīng)歷過Scavenge的GC的邏輯是,每次GC時給存活對象的age屬性+1,當(dāng)再次GC的時候判斷age屬性即可?;镜臅x升示意圖如下所示:

V8的內(nèi)存管理與垃圾回收算法是什么

老生代內(nèi)存中,長期存活的對象較多,無法采取Scavenge算法回收的原因在于:

  • 存活對象較多導(dǎo)致復(fù)制效率低下

  • 浪費了一半的內(nèi)存空間

老生代內(nèi)存對象的回收算法

老生代內(nèi)存空間的垃圾回收采用的是標(biāo)記清除Mark-Sweep)和標(biāo)記整理Mark-Compact)結(jié)合的方式。標(biāo)記清除分為兩部分:

  • 標(biāo)記階段

  • 清除階段(如果是標(biāo)記整理則是整理階段)

在標(biāo)記階段遍歷老生代堆內(nèi)存中的所有內(nèi)存對象,并對活著的對象做標(biāo)記,清除階段只清理未被標(biāo)記的對象。原因是:老生代內(nèi)存中非存活對象占少數(shù)。

V8的內(nèi)存管理與垃圾回收算法是什么

如上圖所示,標(biāo)記清除存在的一個問題是清理之后存在了不連續(xù)的空間導(dǎo)致無法繼續(xù)利用,所以對于老生代內(nèi)存空間的內(nèi)存清理需要結(jié)合標(biāo)記整理的方案。該方案是在標(biāo)記過程中將活著的對象往一側(cè)移動,移動完成后再清理界外的所有非存活對象移除。

V8的內(nèi)存管理與垃圾回收算法是什么

垃圾回收的全暫停

垃圾回收時需要暫停應(yīng)用執(zhí)行邏輯,待垃圾回收機制結(jié)束后再恢復(fù)應(yīng)用執(zhí)行邏輯,該行為稱為“全暫停”,也就是常說的Stop The World,簡稱STW。對新生代內(nèi)存的垃圾回收該行為對應(yīng)用執(zhí)行影響不大,但是老生代內(nèi)存由于存活對象較多,所以老生代內(nèi)存的垃圾回收造成的全停頓影響非常大。

V8的內(nèi)存管理與垃圾回收算法是什么

V8為了優(yōu)化GC的全暫停時間,還引入了增量標(biāo)記并發(fā)標(biāo)記、并行標(biāo)記、增量整理、并行清理、延遲清理等方式。

STW優(yōu)化

衡量垃圾回收所用時間的一個重要指標(biāo)是執(zhí)行 GC 時主線程暫停的時間量。STW所帶來的影響是無法接受的,因此V8也采取的很多優(yōu)化手段。

  • 并行GC

GC的過程需要做大量的事情從而在主線程上導(dǎo)致STW現(xiàn)象,并行GC的做法是開多個輔助線程分擔(dān)GC的事情。該做法依然無法避免STW現(xiàn)象的,但是可以減少STW的總時間,取決于開啟的輔助線程數(shù)量。

V8的內(nèi)存管理與垃圾回收算法是什么

  • 增量GC

增量GC將GC工作進行拆分,并在主線程中間歇的分步執(zhí)行。該做法并不會減少GC的時間,相反會稍微花銷,但是它同樣會減少GC的STW的總時間。

V8的內(nèi)存管理與垃圾回收算法是什么

  • 并發(fā)GC

并發(fā)GC是指GC在后臺運行,不再在主線程運行。該做法會避免STW現(xiàn)象。

V8的內(nèi)存管理與垃圾回收算法是什么

  • 空閑時間GC

Chrome中動畫的渲染大約是60幀(每幀約16ms),如果當(dāng)前渲染所花費時間每達到16.6ms,此時則有空閑時間做其他事情,比如部分GC任務(wù)。

V8的內(nèi)存管理與垃圾回收算法是什么

減少垃圾回收的影響

想要提高執(zhí)行效率要盡量減少垃圾回收的執(zhí)行和消耗:

  • 慎把內(nèi)存當(dāng)作緩存,小心把對象當(dāng)作緩存,要合理限制過期時間和無限增長的問題,可以采用lru策略

  • Node中避免使用內(nèi)存存儲用戶會話,否則在內(nèi)存中存放大量用戶會話對象導(dǎo)致老生代內(nèi)存激增,影響清理性能進而影響應(yīng)用執(zhí)行性能和內(nèi)存溢出。改進方式使用使用redis等。將緩存轉(zhuǎn)移到外部的好處:

    • 減少常駐內(nèi)存對象的數(shù)量,垃圾回收更高效

    • 進程之間可以共享緩存

以上就是“V8的內(nèi)存管理與垃圾回收算法是什么”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學(xué)習(xí)更多的知識,請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

v8
AI