JavaScript垃圾回收機制

垃圾數(shù)據(jù)

被使用之后,可能就不再需要的數(shù)據(jù)稱為垃圾數(shù)據(jù)需要對這些垃圾數(shù)據(jù)進行回收,以釋放有限的內(nèi)存空間**

垃圾回收策略

垃圾數(shù)據(jù)回收分為手動回收和自動回收兩種策略

手動回收

C/C++ 就是使用手動回收策略,何時分配內(nèi)存、何時銷毀內(nèi)存都是由代碼控制的(malloc、free函數(shù)),要使用堆中的一塊空間,需要先調用 mallco 函數(shù)分配內(nèi)存,然后再使用,當數(shù)據(jù)已經(jīng)不再需要了,沒有主動調用 free 函數(shù)來銷毀,容易內(nèi)存泄漏

自動回收

自動垃圾回收的策略,如 JavaScript、Java、Python 等語言,產(chǎn)生的垃圾數(shù)據(jù)是由垃圾回收器來釋放的

執(zhí)行到函數(shù)時,JavaScript 引擎會創(chuàng)建函數(shù)的執(zhí)行上下文****,并壓入棧中。與此同時,還有一個記錄當前執(zhí)行狀態(tài)的指針(稱為 ESP),指向調用棧中正在執(zhí)行的函數(shù)的執(zhí)行上下文。函數(shù)執(zhí)行完進入下一個函數(shù),JavaScript 會通過將ESP下移到下一個函數(shù)的執(zhí)行上下文,可以銷毀上一個函數(shù)的執(zhí)行上下文

調用棧中的數(shù)據(jù)回收

當一個函數(shù)執(zhí)行結束之后,JavaScript 引擎會通過向下移動ESP來銷毀該函數(shù)保存在棧中的執(zhí)行上下文

堆中的數(shù)據(jù)回收

函數(shù)執(zhí)行結束之后,ESP 應該是指向全局執(zhí)行上下文的,函數(shù)的執(zhí)行上下文就處于無效狀態(tài),不過保存在堆中的對象依然占用著空間,要回收堆中的垃圾數(shù)據(jù),就需要用到 JavaScript 中的垃圾回收器了

代際假說和分代收集

后續(xù)垃圾回收的策略都是建立在代際假說的基礎之上的,有兩個特點:

第一個是大部分對象在內(nèi)存中存在的時間很短,簡單來說,就是很多對象一經(jīng)分配內(nèi)存,很快就變得不可訪問;

第二個是不死的對象,會活得更久。

V8 是如何實現(xiàn)垃圾回收?

在 V8 中會把堆分為新生代和老生代兩個區(qū)域

l 新生代中存放的是生存時間短的對象,老生代中存放的生存時間久的對象。

l 新生區(qū)通常只支持 1~8M 的容量,而老生區(qū)支持的容量就大很多了。

l 多數(shù)小的對象都會被分配到新生區(qū);除了新生區(qū)中晉升的對象,一些大的對象會直接被分配到老生區(qū)。

l 新生區(qū)和老生區(qū)標記過程是同一個過程,之后新生代把存活的數(shù)據(jù)移動到空閑區(qū),老生代把死去的對象加到空閑列表中

l 對于這兩塊區(qū)域,V8 分別使用兩個不同的垃圾回收器,以便更高效地實施垃圾回收

副垃圾回收器,主要負責新生代的垃圾回收。主垃圾回收器,主要負責老生代的垃圾回收。不論什么類型的垃圾回收器,它們都有一套共同的執(zhí)行流程

垃圾回收器的工作流程

第一步是標記空間中活動對象和非活動對象。所謂活動對象就是還在使用的對象,非活動對象就是可以進行垃圾回收的對象。

第二步是回收非活動對象所占據(jù)的內(nèi)存。其實就是在所有的標記完成之后,統(tǒng)一清理內(nèi)存中所有被標記為可回收的對象。

第三步是做內(nèi)存整理。一般來說,頻繁回收對象后,內(nèi)存中就會存在大量不連續(xù)空間,稱為內(nèi)存碎片。如果需要分配較大連續(xù)內(nèi)存的時候,就可能內(nèi)存不足。內(nèi)存碎片的整理是可選的,因為有的垃圾回收器不會產(chǎn)生內(nèi)存碎片(副垃圾回收器)

副垃圾回收器

負責新生區(qū)的垃圾回收,新生代中用Scavenge 算法來處理。

Scavenge 算法,是把新生代空間對半劃分為兩個區(qū)域,一半是對象區(qū)域,一半是空閑區(qū)域。

新加入的對象都會存放到對象區(qū)域,當對象區(qū)域快被寫滿時,就需要執(zhí)行一次垃圾清理操作。

在垃圾回收過程中,首先要對對象區(qū)域中的垃圾做標記;標記完成之后,就進入垃圾清理階段,副垃圾回收器會把這些存活的對象復制到空閑區(qū)域中,并有序地排列起來,相當于內(nèi)存整理操作,復制后空閑區(qū)域就沒有內(nèi)存碎片了。

完成復制后,對象區(qū)域與空閑區(qū)域進行角色翻轉,也就是原來的對象區(qū)域變成空閑區(qū)域,原來的空閑區(qū)域變成了對象區(qū)域,是使得新生代中的這兩塊區(qū)域無限重復使用下去。

因此,新生區(qū)中垃圾回收還是比較頻繁,復制操作需要時間成本,如果新生區(qū)空間太大,那么每次清理的時間就會過久,所以為了執(zhí)行效率,一般新生區(qū)的空間會被設置得比較小。但也因此很容易被存活的對象裝滿整個區(qū)域。為了解決這個問題,JavaScript 引擎采用了對象晉升策略,也就是經(jīng)過兩次垃圾回收依然還存活的對象,會被移動到老生區(qū)中。

主垃圾回收器

負責老生區(qū)中的垃圾回收,老生區(qū)中的對象有兩個特點,一個是對象占用空間大,另一個是對象存活時間長(晉升對象),因此不適合用 Scavenge 算法(時間和空間成本)

主垃圾回收器是采用標記 - 清除(Mark-Sweep)的算法進行垃圾回收的

標記

從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾數(shù)據(jù)。也就是遍歷調用棧,能否找到引用某個地址的變量,不能時說明該地址存放的是垃圾數(shù)據(jù)。

wps5-1599304715705.jpg

清除

它和副垃圾回收器的垃圾清除過程完全不同,這個是直接對可回收對象進行清理的過程。

對一塊內(nèi)存多次執(zhí)行標記 - 清除算法后,會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,于是又產(chǎn)生了另外一種算法——標記 - 整理。

標記過程不變,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存

wps6-1599304715706.jpg

因此,主垃圾回收器執(zhí)行一次完整的垃圾回收流程為標記-清理-整理。

全停頓

從上面了解了,V8 是使用副垃圾回收器和主垃圾回收器處理垃圾回收的,不過由于JavaScript 是運行在主線程之上的,一旦執(zhí)行垃圾回收算法,都需要將正在執(zhí)行的JavaScript 腳本暫停下來,待****垃圾回收完畢后再恢復腳本執(zhí)行,這種行為叫做全停頓(Stop-The-World)

wps7-1599304715706.jpg

V8 新生代的垃圾回收中,因其空間較小,且存活對象較少,所以全停頓的影響不大****,但老生代就不一樣,執(zhí)行動畫時,會因為老生代的垃圾回收而造成的卡頓

因此,V8 將標記過程分為一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,我們把這個算法稱為增量標記算法。

wps8.jpg

這樣就可以把一個完整的垃圾回收任務拆分為很多小的任務,穿插在其他的 JavaScript 任務中間執(zhí)行,避免用戶因為垃圾回收任務而感受到頁面的卡頓。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容