天天看點

V8 垃圾回收機制

垃圾回收政策

垃圾回收有手動回收和自動回收兩種政策。

手動回收需要自己控制記憶體的配置設定和銷毀,如果配置設定了記憶體在使用結束後沒有進行銷毀,會造成記憶體洩漏。

而 JavaScript 采用的是另外一種政策即通過垃圾回收器自動回收的機制。

因為 JavaScript 中的資料存儲在棧(原始資料類型)和堆(引用資料類型)中,是以它的垃圾回收也包含棧中的垃圾回收和堆中的垃圾回收兩種。

棧中的垃圾回收

通過下面的一段代碼,我們可以先看下調用棧中的垃圾是如何回收的。

function fn() {
    let num1 = 1;
    let obj1 = {test: "haha"};
    function fn2() {
        let num2 = 2;
        let obj2 = {test: "xixi"};
    }
    fn2();
}
fn();           

執行到 fn2 時,此時調用棧和堆的狀态如下圖所示

V8 垃圾回收機制

從圖中可以看出,原始類型的資料是配置設定在棧中的,對象這些引用類型的資料是配置設定在堆中的。

當 fn2 執行完的時候,fn2 所對應的棧中的資料就會被銷毀掉,它是如何被銷毀的呢?

在棧中有一個記錄目前執行狀态的指針 ESP,它指向目前執行函數的上下文,當 fn2 執行完的時候,這個指針就會下移到 fn 的執行上下文,這個下移的操作就是銷毀 fn2 執行上下文的過程。

如果之後還有别的函數執行,那麼該函數的執行上下文就會直接覆寫在原來 fn2 執行上下文的地方。

即當一個函數執行結束之後,JS 引擎會通過向下移動 ESP 來銷毀該函數儲存在棧中的執行上下文。

堆中的垃圾回收

當棧中的資料被回收掉之後,接下來我們看看堆中的資料是如何進行回收的。

要回收堆中的垃圾資料,就需要用到 JavaScript 的垃圾回收器。

V8 引擎把堆分為新生代和老生代兩個區域,新生代中存放的是生存時間短的資料,老生代中存放的是生存時間長的資料。

新生區出于效率的考慮,一般比較小,隻有 1-8 M 的容量,而老生區容量則大很多。這兩個區域使用的垃圾回收器是不同的。新生代的垃圾是通過副垃圾回收器回收的,而老生代的垃圾是通過主垃圾回收器進行回收的。

垃圾回收器的工作流程

垃圾回收器的工作流程其實都是差不多的。

第一步是先對空間中的對象進行标記,将空間中的對象分為活動對象和非活動對象,非活動對象就是可以進行垃圾回收的對象。

第二步是回收非活動對象所占據的記憶體。就是在所有的标記都完成之後,對記憶體中所有标記為非活動對象進行清理。

第三步是進行記憶體整理。垃圾回收後,記憶體中可能會出現不連續的空間,如果之後需要配置設定較大的連續記憶體,那麼這些不連續的空間就可能導緻配置設定不了,是以我們需要對記憶體進行整理。不過記憶體整理這一步不是一定需要的,因為有些垃圾回收器在進行回收的時候不會産生記憶體碎片,例如我們下面要說到的副垃圾回收器。

副垃圾回收器

副垃圾回收器負責新生區的垃圾回收。除了占用記憶體比較大的對象,一般的對象都是配置設定到新生區的,是以新生區的垃圾回收相對老生區來說會更頻繁。

副垃圾回收器使用 Scavenge 算法進行處理。該算法将新生區的空間進行對半劃分,一個作為存儲對象的區域,另外一個作為空閑區域。

新加入的對象都會放到對象區域,當對象區域快要滿了的時候,就會執行垃圾回收。

副垃圾回收器先對對象區域中的垃圾進行标記,标記完成後就将活動對象有序地複制到空閑區域中,這樣在垃圾清理的過程中也相當于完成了記憶體的整理,複制後的空閑區域就變成對象區域,而原來的對象區域則變成了空閑區域。這樣就完成了垃圾的回收。

每當對象區域快滿了的時候就會進行垃圾回收,是以對象區域和空閑區域也是不斷進行翻轉,可以無限重複地使用下去。

那麼會有一個問題,新生區的空間不大,如果被裝滿了怎麼辦?為了解決這個問題,V8 引擎使用了對象晉升政策,如果一個對象經過兩次垃圾回收依然存活着,那麼它就會被移動到老生區中。

主垃圾回收器

從上面對副垃圾回收器的介紹中,我們可以看出來,老生區中的對象主要是占用記憶體比較大的對象和存活時間久的對象,其中占用記憶體大的對象是直接配置設定到老生區的。

因為老生區的對象比較大,如果采用和副垃圾回收器一樣的回收政策的話,複制的操作需要花費比較長的時間,并且有一半的空間會被浪費掉,是以主垃圾回收器采用的是另外一種算法,标記-整理算法來進行垃圾回收。

首先是從一組根元素開始,遞歸周遊這組根元素,能周遊到的對象标記為活動對象,沒有周遊到的對象标記為需要進行垃圾回收的非活動資料。

标記完之後就将所有活動對象向同一邊進行移動,移動完之後就将端邊界外的記憶體都清理掉,這樣就完成了記憶體整理和垃圾回收。

增量标記算法

因為 JavaScript 是單線程的,在執行垃圾回收的時候無法執行其他任務,是以如果垃圾回收時間過長的話那麼就會造成明顯的卡頓。

因為新生區的空間較小,是以造成的影響也比較小,我們可以忽略不計,而老生區因為其空間比較大,是以垃圾回收的時間可能過長。

為了降低老生區垃圾回收造成的卡頓,V8 将垃圾回收中的标記過程分為一個個子标記過程,同時讓垃圾回收标記和 JavaScript 邏輯的交替執行,直到标記階段完成,這個算法就是增量标記算法。

通過将一個大的任務劃分為一個個小的任務,就可以讓垃圾回收和 JavaScript 邏輯都正常地進行,同時不影響使用者的體驗。