V8 JavaScript引擎研究(三)垃圾回收器的实现
V8垃圾回收機(jī)制簡介
V8垃圾回收器的實(shí)現(xiàn),是V8高效的一個(gè)非常重要的原因。
V8在運(yùn)行時(shí)自動(dòng)回收不再需要使用的對象內(nèi)存,也即是垃圾回收。
V8使用了全暫停式(stop-the-world)、分代式(generational)、精確(accurate)等組合的垃圾回收機(jī)制,來確保更快的對象內(nèi)存分配、更短的垃圾回收時(shí)觸發(fā)的暫停以及沒有內(nèi)存碎片。
V8的垃圾回收有如下幾個(gè)特點(diǎn):
- 當(dāng)處理一個(gè)垃圾回收周期時(shí),暫停所有程序的執(zhí)行。
- 在大多數(shù)垃圾回收周期,每次僅處理部分堆中的對象,使暫停程序所帶來的影響降至最低。
- 準(zhǔn)確知道在內(nèi)存中所有的對象及指針,避免錯(cuò)誤地把對象當(dāng)成指針?biāo)鶐淼膬?nèi)存泄露。
在V8中,對象堆被分為兩個(gè)部分:新創(chuàng)建的對象所在的新生代,以及在一個(gè)垃圾回收周期后存活的對象被提升到的老生代。如果一個(gè)對象在一個(gè)垃圾回收周期中被移動(dòng),那么V8將會(huì)更新所有指向此對象的指針。
V8垃圾回收機(jī)制深入剖析
如大多數(shù)動(dòng)態(tài)類型語言一樣,JavaScript使用垃圾回收機(jī)制來管理內(nèi)存,然而ECMAScript沒有任何操作垃圾回收的接口,所有的操作都有JavaScript引擎自動(dòng)完成。這讓開發(fā)者省卻了手動(dòng)管理內(nèi)存分配所帶來的各種棘手問題。
V8在實(shí)現(xiàn)JavaScript的垃圾回收機(jī)制時(shí),使用了各種非常復(fù)雜的策略,保證了高效的內(nèi)存使用及回收,這也是Node之所以選擇V8作為JavaScript引擎的原因之一。如果V8僅會(huì)在瀏覽器中被使用,那么即使發(fā)生內(nèi)存泄漏等各種內(nèi)存問題也僅僅會(huì)影響到一個(gè)終端用戶,而且考慮到V8引擎在瀏覽器中的生命周期不長,一旦頁面被關(guān)閉后,內(nèi)存即會(huì)被釋放。但如果在服務(wù)器端使用V8(使用Node作為服務(wù)器環(huán)境),那么內(nèi)存問題所造成的影響將會(huì)波及到大量終端用戶,顯然V8需要高效的內(nèi)存管理機(jī)制。
V8會(huì)限制所使用的內(nèi)存大小,原因一是最初V8是作為瀏覽器JavaScript引擎所設(shè)計(jì)的,二是V8的垃圾回收機(jī)制的限制,如果內(nèi)存分配過大,那么垃圾回收周期也就越長,對程序造成的影響也就越大。V8的內(nèi)存大小限制是可配置的。
V8的堆組成
V8的堆由一系列區(qū)域組成:
- 新生代區(qū):大多數(shù)對象的創(chuàng)建被分配在這里,這個(gè)區(qū)域很小,但垃圾回收非常頻繁,獨(dú)立于其它區(qū)。
- 老生代指針區(qū):包含大部分可能含有指向其它對象指針的對象。大多數(shù)從新生代晉升(存活一段時(shí)間)的對象會(huì)被移動(dòng)到這里。
- 老生代數(shù)據(jù)區(qū):包含原始數(shù)據(jù)對象(沒有指針指向其它對象)。Strings、boxed numbers以及雙精度unboxed數(shù)組從新生代中晉升后被移到這里。
- 大對象區(qū):這里存放大小超過其它區(qū)的大對象。每個(gè)對象都有自己mmap內(nèi)存。大對象不會(huì)被回收。
- 代碼區(qū):代碼對象(即包含被JIT處理后的指令對象)存放在此。唯一的有執(zhí)行權(quán)限的區(qū)域(代碼過大也可能存放在大對象區(qū),此時(shí)它們也可被執(zhí)行,但不代表大對象區(qū)都有執(zhí)行權(quán)限)。
- Cell區(qū)、屬性Cell區(qū)以及map區(qū):包含cell、屬性cell以及map。每個(gè)區(qū)都存放他們指向的相同大小以及相同結(jié)構(gòu)的對象。
每個(gè)區(qū)都由一系列的內(nèi)存頁(page)組成。內(nèi)存頁是一塊聯(lián)系的內(nèi)存,使用操作系統(tǒng)的mmap(或Windows的等價(jià)物)分配。每個(gè)區(qū)的頁的大小都是1MB,而且以1MB對齊,除了大對象區(qū)(那里可能更大)。除了存儲(chǔ)對象,內(nèi)存頁還會(huì)包含一個(gè)頭部信息(包括一些標(biāo)志和元數(shù)據(jù))以及一個(gè)記號位圖(指明哪些對象是活著的)。每個(gè)頁還有一個(gè)分配在單獨(dú)內(nèi)存中的槽緩沖區(qū),放置一組對象,這些對象可能指向在當(dāng)前頁的其他對象。
識別指針
任何垃圾回收器首要解決的任務(wù)就是區(qū)分開出指針和數(shù)據(jù)。垃圾回收器需要循著指針來找到活著的對象。多數(shù)垃圾回收算法都會(huì)在內(nèi)存中移動(dòng)對象的位置(為了減少內(nèi)存碎片以及使內(nèi)存更緊湊),所以需要可以改寫指針指向的位置并且不破壞舊的數(shù)據(jù)。
目前有三種主流的方法來識別指針:
- 保守法(Conservative)。可在沒有編譯器支持的情況下實(shí)現(xiàn)。基本上,將堆上所有對齊的字都認(rèn)為是指針,這意味著一些數(shù)據(jù)也會(huì)被當(dāng)成是指針。因此,當(dāng)一個(gè)整數(shù)被當(dāng)成指針時(shí),可能導(dǎo)致原本應(yīng)該被回收的內(nèi)存被誤認(rèn)為還有指針(實(shí)際只是個(gè)整數(shù))指向它而沒有被回收,這會(huì)產(chǎn)生非常奇怪的內(nèi)存泄漏現(xiàn)象。我們不能移動(dòng)內(nèi)存中的任何對象,因?yàn)楫?dāng)錯(cuò)把整數(shù)當(dāng)成指針時(shí)會(huì)改變數(shù)據(jù)本身,這將導(dǎo)致垃圾回收的內(nèi)存整理算法不會(huì)產(chǎn)生任何好處。(內(nèi)存連續(xù)的好處顯而易見:更容易分配大的內(nèi)存,省卻了內(nèi)存碎片導(dǎo)致的不斷查找,cache緩存命中的幾率高)。
- 編譯器提示法(Compiler hints)。如果使用靜態(tài)類型語言,則編譯器可以準(zhǔn)確告知每個(gè)類中指針的偏移地址。只要能知道一個(gè)對象實(shí)現(xiàn)自哪個(gè)類,就可找出它的全部指針。Java虛擬機(jī)使用了此種方法,然而這種方案對于動(dòng)態(tài)類型語言(如JavaScript)來說確不切實(shí)際,因?yàn)橐粋€(gè)對象中的屬性既可以是指針也可以是數(shù)據(jù)。
- 指針標(biāo)記法(Tagged pointers)。此種方法需要在每個(gè)字(word)的末位保留一位來指明其是指針還是數(shù)據(jù)。這種方法有編譯器支持的限制,但實(shí)現(xiàn)簡單。V8使用了這種方法,一些靜態(tài)類型語言也使用了這種方法,如OCamI。V8將所有數(shù)據(jù)以32bit字寬來存儲(chǔ),其中最低一位為0,而指針的最低兩位為01。
V8將屬于-230至230-1范圍內(nèi)的所有小整數(shù)(V8內(nèi)部用Sims表示)使用了一個(gè)最低位為0的32位字(word)來表示,指針的最低兩位為01。由于所有對象都至少以4個(gè)字節(jié)(byte)對齊,因此這樣實(shí)現(xiàn)沒有問題。在堆上的絕大多數(shù)對象都包含一組標(biāo)記后的字(word),所以垃圾回收器可以快速的掃描它們,找出指針,忽略整數(shù)。有些對象,如string,已知僅包含數(shù)據(jù)(沒有指針),它們的內(nèi)容可以不被標(biāo)記。
分代式回收
在大多數(shù)程序中,絕大多數(shù)對象的生命周期很短,只有少部分對象擁有較長的生命周期。
考慮到這一點(diǎn),V8將堆分成了兩代:新生代(new-space)及老生代(old-space)。
對象在新生代中被創(chuàng)建,新生代很小,只有1~8MB。在新生代中分配內(nèi)存非常容易,持有一個(gè)指向內(nèi)存區(qū)的分配指針,當(dāng)需要給新對象分配內(nèi)存時(shí),遞增指針即可。當(dāng)分配指針到達(dá)了新生代的末尾,就會(huì)觸發(fā)一個(gè)小垃圾回收周期(Scavenge算法),快速地從新生代中回收死去的對象。對于在兩個(gè)小垃圾回收周期都存活下來的對象,則將其晉升(promote)至老生代。老生代在標(biāo)記-清除(mark-sweep)或標(biāo)記-整理(mark-compact)這樣的非常不頻繁的大垃圾回收周期中回收內(nèi)存。當(dāng)足夠數(shù)量的對象被晉升至老生代中后,將觸發(fā)一次大垃圾回收周期,至于具體時(shí)機(jī),取決于老生代的內(nèi)存大小及程序的行為。
新生代垃圾回收算法
由于新生代中的垃圾回收非常頻繁,因此回收算法必須足夠快。V8的新生代垃圾回收使用了Scavenge算法(Cheney算法的一種實(shí)現(xiàn))。
新生代被劃分為兩個(gè)大小相等的子區(qū):to區(qū)及from區(qū)。絕大多數(shù)對象的內(nèi)存分配都發(fā)生在to區(qū)(一些特定類型的對象,如可執(zhí)行的代碼對象,被分配在老生代中)。當(dāng)to區(qū)被填滿后,交換to區(qū)和from區(qū),現(xiàn)在所有對象都存儲(chǔ)在from區(qū),然后將活著的對象從from區(qū)拷貝到to區(qū)或者將它們晉升至老生代中,拷貝的過程實(shí)際是對to區(qū)內(nèi)存的一次整理過程,整理后to區(qū)的內(nèi)存占用將是連續(xù)的,以便于下次的內(nèi)存分配依然保持快速和簡單。拷貝過后,釋放from區(qū)的內(nèi)存。
可以看出,在新生代的垃圾回收周期中,始終有一半的內(nèi)存時(shí)空的,由于新生代很小,因此浪費(fèi)的空間并不大,而且新生代中的絕大部分對象生命周期都很短,因此需要復(fù)制的活著的對象很少,所以時(shí)間效率極高。
新生代垃圾回收特點(diǎn):
- 內(nèi)存區(qū)小
- 浪費(fèi)一半空間
- 垃圾多
- 拷貝范圍小
- 執(zhí)行頻率高且快
算法具體執(zhí)行過程:
在to區(qū)中維護(hù)兩個(gè)指針:allocationPtr(指向?yàn)橄乱粋€(gè)對象分配內(nèi)存的地址)及scanPtr(指向下一個(gè)需要掃描的對象地址)。
首先,to區(qū)填滿,交換to區(qū)與from區(qū),此時(shí)to區(qū)為空,from區(qū)為滿。
將from區(qū)中所有可從根對象到達(dá)的對象拷貝至to區(qū),可以把此時(shí)的to區(qū)想象成一個(gè)雙端隊(duì)列,scanPtr指向隊(duì)首,allocationPtr指向隊(duì)尾,然后執(zhí)行循環(huán),每次遍歷一個(gè)to區(qū)的對象,即每次遍歷一個(gè)scanPtr所指向的對象,然后自增scanPtr。遍歷到一個(gè)對象時(shí),依次分析此對象內(nèi)部的所有指針,如果指針不指向from區(qū)里的對象,則忽略它(因?yàn)榇藭r(shí)它必指向老生代中的對象,而老生代中的對象還未回收);如果指針指向from區(qū)中的對象,并且此對象還未被復(fù)制到to區(qū)(還未設(shè)置轉(zhuǎn)換地址,具體見后文),則將其復(fù)制到to區(qū)的末端,也即allocationPtr所指向的位置,同時(shí)自增allocationPtr。然后將from區(qū)中的此對象的轉(zhuǎn)換地址設(shè)置為復(fù)制后to區(qū)中的位置。轉(zhuǎn)換地址保存在此對象的第一個(gè)字中(word),代替map地址。垃圾回收器可以通過檢查對象第一個(gè)字中的低位來區(qū)分出轉(zhuǎn)換地址和map地址,map地址的低位被標(biāo)記,而轉(zhuǎn)換地址沒有。后續(xù)當(dāng)分析到一個(gè)指針指向一個(gè)在from區(qū)中的對象,并且此對象已經(jīng)被復(fù)制到to區(qū)(已有轉(zhuǎn)換地址),那么只需簡單將此指針指向的地址更新為對象的轉(zhuǎn)換地址即可。
當(dāng)所有to區(qū)中的對象都被處理后,即scanPtr與allocationPtr相遇時(shí),算法結(jié)束。此時(shí)from中的所有對象都是垃圾對象,可以被全部釋放。
抽象來看,整個(gè)算法遍歷過程實(shí)際是一個(gè)BFS(廣度優(yōu)先搜索)過程,scanPtr之前所有對象都是其內(nèi)部指針已被分析完畢的對象,scanPtr與allocationPtr之間的對象是需要分析內(nèi)部指針的對象,當(dāng)scanPtr與allocationPtr重合,搜索結(jié)束。
算法偽代碼如下:
def scavenge():swap(fromSpace, toSpace)allocationPtr = toSpace.bottomscanPtr = toSpace.bottomfor i = 0..len(roots):root = roots[i]if inFromSpace(root): rootCopy = copyObject(&allocationPtr, root) setForwardingAddress(root, rootCopy) roots[i] = rootCopy while scanPtr < allocationPtr: obj = object at scanPtr scanPtr += size(obj) n = sizeInWords(obj) for i = 0..n: if isPointer(obj[i]) and not inOldSpace(obj[i]): fromNeighbor = obj[i] if hasForwardingAddress(fromNeighbor): toNeighbor = getForwardingAddress(fromNeighbor) else: toNeighbor = copyObject(&allocationPtr, fromNeighbor) setForwardingAddress(fromNeighbor, toNeighbor) obj[i] = toNeighbor def copyObject(*allocationPtr, object): copy = *allocationPtr *allocationPtr += size(object) memcpy(copy, object, size(object)) return copy
寫屏障
上面的算法有一個(gè)問題被忽略了:如果新生代中的某個(gè)對象,只有一個(gè)指針指向它,并且此指針屬于老生代中的某個(gè)對象,那么如何識別出新生代中的此對象不是垃圾對象。將老生代中的全部對象掃描一遍是不現(xiàn)實(shí)的,因?yàn)閿?shù)量龐大消耗巨大。
為了解決這個(gè)問題,V8在存儲(chǔ)緩沖區(qū)中維護(hù)了一個(gè)列表,記錄了老生代對象指向新生代對象的情況。當(dāng)一個(gè)新對象被創(chuàng)建時(shí),沒有指針指向它,但當(dāng)一個(gè)老生代中的對象指向它時(shí),便在列表中記錄下來,由于每次寫操作都需要執(zhí)行這樣一個(gè)過程,這也稱作寫屏障。
每次在發(fā)生一個(gè)指針指向?qū)ο筮@樣的寫操作時(shí)都需要執(zhí)行這樣一系列的額外指令,這就是垃圾回收機(jī)制所帶來的代價(jià),但是代價(jià)并不大,寫操作相比讀操作并不常發(fā)生。一些其它JavaScript引擎的垃圾回收算法使用了讀屏障,但這需要硬件輔助才能有較低的消耗。V8采用了一些機(jī)制來優(yōu)化寫屏障所帶來的消耗:
- 通常可以驗(yàn)證出一個(gè)對象位于新生代,寫屏障不會(huì)發(fā)生在新生代對象的操作上。
- 一個(gè)對象的所有引用都發(fā)生在局部時(shí),此對象會(huì)被分配在棧上,棧上的對象顯然不需要寫屏障。
- 老->新這樣的情況很少見,因此快速檢測出新->新及老->老這兩種常見情況可以提升很大性能,因?yàn)榇藘煞N情況不需要寫屏障。每個(gè)頁都以1MB來對齊,因此通過過濾一個(gè)對象的低20位(bit),可以找到它所在的頁。頁頭含有指向其是否在新舊生代的標(biāo)識,因此可以快速檢查出兩個(gè)對象所處的生代。
- 當(dāng)存有關(guān)系列表的存儲(chǔ)緩沖區(qū)被填滿后,V8將對其排序,合并相同的項(xiàng)目,移除指針不再指向新生代的情況。
老生代垃圾回收算法
Scavenge算法對于少量內(nèi)存具有非常快的回收和整理能力,但有很大的內(nèi)存開銷,因?yàn)橐瑫r(shí)支持to區(qū)及from區(qū)。這對于少量內(nèi)存區(qū)是可以接受的,但對于超過數(shù)MB的內(nèi)存區(qū)來說,使用Scavenge算法是不切實(shí)際的。因此對于老生代數(shù)百M(fèi)B的內(nèi)存空間,V8使用了兩種緊密相連的算法,即標(biāo)記-清除算法與標(biāo)記-整理算法。
這兩種算法都包含了兩個(gè)階段:標(biāo)記階段,清除階段或整理階段。
標(biāo)記清除(Mark-Sweep)
遍歷堆中所有的對象,標(biāo)記所有活著的對象。在清除階段,只清除沒有被標(biāo)記的對象,即垃圾對象。由于垃圾對象占比很小,因此效率很高。
標(biāo)記清除所帶來的問題是,當(dāng)一次清除結(jié)束后,內(nèi)存空間往往出現(xiàn)了很多碎片,導(dǎo)致內(nèi)存不再連續(xù)。當(dāng)需要分配一個(gè)占有大量內(nèi)存的對象時(shí),如果沒有一個(gè)內(nèi)存碎片能滿足此對象所需空間的大小,那么V8將無法完成此次分配,導(dǎo)致出發(fā)垃圾回收。
標(biāo)記整理(Mark-Compact)
為了解決標(biāo)記清除后所帶來的內(nèi)存碎片問題,V8引入了標(biāo)記整理。標(biāo)記整理的標(biāo)記階段與標(biāo)記清除一樣,但清除階段變?yōu)檎黼A段:將活著的對象向內(nèi)存區(qū)的一段移動(dòng),移動(dòng)完后直接清除邊界外的內(nèi)存。整理階段涉及對象的移動(dòng),因此效率不高,但能保證不會(huì)產(chǎn)生內(nèi)存碎片。
老生代垃圾回收特點(diǎn):
- 內(nèi)存區(qū)大
- 存放的對象生命周期很長
- 被清除的對象少,回收范圍小
- 執(zhí)行頻率低且較慢
算法具體執(zhí)行過程:
在標(biāo)記階段,所有在堆上活著的對象都會(huì)被發(fā)現(xiàn)且被標(biāo)記。每個(gè)頁都包含一個(gè)標(biāo)記位圖,位圖的每一位都對應(yīng)頁上的一個(gè)字(一個(gè)指針大小就是一個(gè)字,也即對象的起始地址的大小就是一個(gè)字),這樣做非常必要,因?yàn)閷ο罂梢云鹗加谌魏巫謱R的偏移地址。位圖會(huì)占據(jù)一定的內(nèi)存開銷(32位系統(tǒng)下占3.1%,64位系統(tǒng)下占1.6%),然而所有的內(nèi)存管理系統(tǒng)都有類似的開銷。同時(shí)使用2個(gè)位(bit)來代表一個(gè)對象的狀態(tài),對象至少占有兩個(gè)字大小,因此這2個(gè)位不會(huì)重疊。
V8使用了三色標(biāo)記法,對象的狀態(tài)可被標(biāo)記成三種(所以用2位來標(biāo)記):
- 如果為白色,那么此對象沒有被垃圾回收器發(fā)現(xiàn);
- 如果為灰色,那么此對象已被垃圾回收器發(fā)現(xiàn),但其鄰接對象還未被處理;
- 如果為黑色,那么此對象已經(jīng)被發(fā)現(xiàn),且所有它的鄰接對象都已被處理完畢;
如果將堆想象成一個(gè)對象間通過指針連接的有向圖,那么標(biāo)記算法本質(zhì)上是一個(gè)DFS(深度優(yōu)先搜索)。在標(biāo)記周期起始時(shí),標(biāo)記位圖被清空,所有的對象都為白色。將可從根部直達(dá)的所有對象都標(biāo)記為灰色,然后放入一個(gè)單獨(dú)分配的雙端隊(duì)列中。接著循環(huán)處理隊(duì)列中的每個(gè)對象:每次垃圾回收器都從隊(duì)列中取出一個(gè)對象并將其標(biāo)記為黑色,接著將其鄰接對象(內(nèi)部指針指向的對象)標(biāo)記為灰色并放入雙端隊(duì)列中。當(dāng)隊(duì)列為空時(shí),所有被發(fā)現(xiàn)的對象都被標(biāo)記成了黑色,此時(shí)算法結(jié)束。對于特別大的對象,如長數(shù)組,在處理時(shí)可能會(huì)被分片處理,以減少隊(duì)列溢出的幾率。如果隊(duì)列發(fā)生溢出,則對象仍會(huì)被標(biāo)記為灰色但不放進(jìn)隊(duì)列中(此時(shí)它們的鄰接對象還未被發(fā)現(xiàn)),當(dāng)隊(duì)列為空時(shí),垃圾回收器會(huì)掃描堆中剩余的灰色對象,然后將它們放入隊(duì)列中,繼續(xù)執(zhí)行標(biāo)記過程。
偽代碼如下:
markingDeque = [] overflow = falsedef markHeap():for root in roots:mark(root)do:if overflow:overflow = false refillMarkingDeque() while !markingDeque.isEmpty(): obj = markingDeque.pop() setMarkBits(obj, BLACK) for neighbor in neighbors(obj): mark(neighbor) while overflow def mark(obj): if markBits(obj) == WHITE: setMarkBits(obj, GREY) if markingDeque.isFull(): overflow = true else: markingDeque.push(obj) def refillMarkingDeque(): for each obj on heap: if markBits(obj) == GREY: markingDeque.push(obj) if markingDeque.isFull(): overflow = true return
當(dāng)標(biāo)記算法結(jié)束后,所有活著的對象都會(huì)被標(biāo)記成黑色,所有死去的對象仍然為白色。這些信息將會(huì)被清除階段或整理階段使用,這取決于接下來使用哪種算法,這兩種算法都以頁級單位來回收內(nèi)存(V8的頁是1MB的連續(xù)內(nèi)存,與虛擬內(nèi)存頁不同)。
清除算法掃描連續(xù)范圍內(nèi)的垃圾對象,然后釋放它們的空間,并將它們加入空閑內(nèi)存鏈表中。每個(gè)頁都包含一些單獨(dú)的空閑內(nèi)存鏈表,分別代表小內(nèi)存區(qū)(<256字)、中內(nèi)存區(qū)(<2048字)、大內(nèi)存區(qū)(<16384字)以及超大內(nèi)存區(qū)。清除算法相當(dāng)簡單,只需遍歷頁的標(biāo)記位圖,找到范圍內(nèi)的白對象,然后釋放。空閑內(nèi)存鏈表被大量用于從新生代Scavenge算法所晉升過來放置在老生代的對象,同時(shí)也被整理所發(fā)用來移動(dòng)對象。有些對象只能分配在老生代,因此空閑內(nèi)存鏈表也被用來分配。
整理算法會(huì)嘗試將對象從碎片頁(包含許多小空閑內(nèi)存的頁)中移動(dòng)并整合到一起。這些對象會(huì)被遷移到別的頁上,因此這時(shí)可能會(huì)需要分配新的頁,而遷出后的頁即可返還給操作系統(tǒng)。整理的過程非常復(fù)雜,大概步驟是,對碎片頁中的每個(gè)活著的對象拷貝到由空間內(nèi)存鏈表分配的一塊內(nèi)存頁,然后在碎片頁的該對象的第一個(gè)字(word)寫上新的轉(zhuǎn)換地址,在之后的遷移過程中,對象的舊地址會(huì)被記錄下來,在遷移結(jié)束后,V8會(huì)遍歷所有記錄有舊地址的指針,然后將這些指針更新為新的轉(zhuǎn)換地址。如果一個(gè)頁非常活躍,有非常多的別的頁的指針指向這個(gè)頁中的對象,那么此頁將會(huì)保留 ,等到下一輪垃圾回收周期再進(jìn)行處理。
V8的老生代內(nèi)存回收結(jié)合了標(biāo)記清除與標(biāo)記整理兩種算法,大部分情況下使用標(biāo)記清除,當(dāng)空間不足以分配從新生代晉升過來的對象時(shí),才使用標(biāo)記整理。
標(biāo)記-清除與標(biāo)記-整理的優(yōu)化
增量標(biāo)記(Incremental Marking)
由于在標(biāo)記清除與標(biāo)記整理的回收期間內(nèi),V8會(huì)暫停所有正在執(zhí)行業(yè)務(wù)的代碼,這會(huì)造成應(yīng)用程序的卡頓,并且隨著堆大小的增長,卡頓的時(shí)間會(huì)急速增加,這顯然是不太可接受的。因此V8在標(biāo)記期間,采用了增量標(biāo)記的方法,將標(biāo)記的過程拆分成很多部分,每次只標(biāo)記一小部分,然后恢復(fù)業(yè)務(wù)代碼的執(zhí)行,再標(biāo)記,這樣循環(huán)交替執(zhí)行標(biāo)記。這樣,原來應(yīng)用程序卡頓的整個(gè)時(shí)間就會(huì)變分拆成多個(gè)細(xì)小的時(shí)間片,極大的提高了應(yīng)用程序的響應(yīng)度。
增量標(biāo)記與標(biāo)準(zhǔn)的標(biāo)記方式的最大區(qū)別在于,在標(biāo)記期間,對象的有向圖會(huì)發(fā)生改變(因?yàn)闀?huì)間歇允許業(yè)務(wù)代碼的執(zhí)行)。因此需要解決的是發(fā)生黑對象指向白對象這種情況。黑對象已經(jīng)被垃圾回收器完全處理過,因此不會(huì)再次對其處理,所以如果發(fā)生這種情況,那么白對象(此時(shí)已非垃圾對象)將依然會(huì)被當(dāng)做垃圾對象回收。V8解決的方法依然是使用寫屏障技術(shù),此時(shí)不僅老生代指向新生代時(shí)發(fā)生寫屏障,當(dāng)黑對象指向白對象也會(huì)觸發(fā)寫屏障,此時(shí)黑對象會(huì)被重新標(biāo)記成灰對象,然后放進(jìn)雙端隊(duì)列中,當(dāng)標(biāo)記算法在稍后處理到隊(duì)列中的該對象時(shí),該對象指向的所有對象都會(huì)被標(biāo)記成灰色,此時(shí)之前的白對象也已變成了灰色,目標(biāo)達(dá)成。
惰性清除(Lazy Sweeping)
當(dāng)增量標(biāo)記完成后,惰性清除開始此時(shí)所有對象都已被標(biāo)記成或生或死,堆已經(jīng)準(zhǔn)確知道可以回收多少內(nèi)存,然而此時(shí)不必去一次全部回收死去的對象,可以采用延遲清理的處理手段,垃圾回收器可以根據(jù)需要來選擇回收部分內(nèi)存, 直到全部垃圾對象回收完畢,整個(gè)增量標(biāo)記-惰性清除的 周期結(jié)束。
更多優(yōu)化
V8已經(jīng)加入了并行清除,主線程不會(huì)操作已死對象,由獨(dú)立的線程來負(fù)責(zé)回收死對象的內(nèi)存,整個(gè)過程只需要非常少量的同步操作。
同時(shí)V8正在實(shí)驗(yàn)并行標(biāo)記,并將在今后引入這一技術(shù)。
總結(jié)
垃圾回收是一項(xiàng)非常復(fù)雜的技術(shù),要實(shí)現(xiàn)高效快速的垃圾回收器需要結(jié)合多種算法以及大量優(yōu)化,幸好引擎已經(jīng)做了全部的工作,開發(fā)者只需關(guān)注更重要的業(yè)務(wù)邏輯即可。
轉(zhuǎn)載于:https://www.cnblogs.com/wolfx/p/5919574.html
總結(jié)
以上是生活随笔為你收集整理的V8 JavaScript引擎研究(三)垃圾回收器的实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 求一个姓袁好听的名字。
- 下一篇: a 标签的跳转属性