簡述JavaScript的垃圾回收機制

穀雨發表於2017-09-10

不管是高階語言,還是低階語言。記憶體的管理都是:

  1. 分配記憶體
  2. 使用記憶體(讀或寫)
  3. 釋放記憶體

前兩步,大家都沒有太大異議。關鍵是釋放記憶體這一步,各種語言都有自己的垃圾回收(garbage collection, 簡稱GC)機制。做GC的第一步是判斷堆中存的是資料還是指標,是指標的話,說明它被指向活躍的物件。有3種判斷方法:

  1. Conservative:如果儲存格式是地址,就認為是。C/C++有用到這種演算法。
  2. Compiler hints:對於靜態語言,比如Java,編譯器是知道它是不是指標的,所以可以用這種。
  3. Tagged pointers:JavaScript用的是這種,在字末位進行標識,1為指標。

對於JavaScript而言,最初的垃圾回收機制,是基於引用計次來做的。後來升級為標記清除。

引用計次

當物件被引用次數為0時,就被回收。潛在的一個問題是:迴圈引用時,兩個物件都至少被引用了一次,將不能自動被回收。所以導致,我們常講的記憶體洩露。

// 引用計次
var a = {t: 1}; // 物件 `{t: 1}` (以下簡稱obj)被引用一次
var b = a; // obj 被引用兩次
a = null; // obj 現在為1次
b = null; // obj 現在為0次,可回收

// 迴圈引用
function fn() {
    var a = {};
    var b = {};
    a.b = b;
    b.a = a;
}

fn();

標記清除

這是當前主流的GC演算法,V8裡面就是用這種。當物件,無法從根物件沿著引用遍歷到,即不可達(unreachable),進行清除。對於上面的例子,fn() 裡面的 ab 在函式執行完畢後,就不能通過外面的上下文進行訪問了,所以就可以清除了。

下面,我們簡述下V8的GC機制:

V8的GC機制

在大部分的應用場景:一個新建立的物件,生命週期通常很短。所以,V8裡面,GC處理分為兩大類:新生代和老生代。

新生代的堆空間為1M~8M,而且被平分成兩份(to-space和from-space),通常一個新建立的物件,記憶體被分配在新生代。當to-space滿的時候,to-space和form-space交換位置(此時,to空,from滿),並執行GC.如果一個物件被斷定為,未被引用,就清除;有被引用,逃逸次數+1(如果此時逃逸次數為2,就移入老生代,否則移入to-space)。

老生代的堆空間大,GC不適合像新生代那樣,用平分成兩個space這種空間換時間的方式。老生代的垃圾回收,分兩個階段:標記、清理(有Sweeping和Compacting這兩種方式)。

標記,採用3色標記:黑、白、灰。步驟如下:

  1. GC開始,所以物件標記為白色。
  2. 根物件標記為黑色,並開始遍歷其子節點(引用的物件)。
  3. 當前被遍歷的節點,標記為灰色,被放入一個叫 marking bitmap 的棧。在棧中,把當前被遍歷的節點,標記為黑色,並出棧,同時,把它的子節點(如果有的話)標記為灰色,並壓入棧。(大物件比較特殊,這裡不展開)
  4. 當所有物件被遍歷完後,就只剩下黑和白。通過Sweeping或Compacting的方式,清理掉白色,完成GC。

小補充:JavaScript的根物件

GC的時候,從根物件開始遍歷。在瀏覽器,根物件是 window;在 Node.js 中,是 global(或稱為root).

root.png

Node.js中,每個檔案被當做一個模組,所以,當你用 var/let/const 在檔案的全域性,宣告變數的時候,作用域是當前檔案(模組)。因此,圖中 root.aundefined.

Links:

相關文章