不管是高階語言,還是低階語言。記憶體的管理都是:
- 分配記憶體
- 使用記憶體(讀或寫)
- 釋放記憶體
前兩步,大家都沒有太大異議。關鍵是釋放記憶體這一步,各種語言都有自己的垃圾回收(garbage collection, 簡稱GC)機制。做GC的第一步是判斷堆中存的是資料還是指標,是指標的話,說明它被指向活躍的物件。有3種判斷方法:
- Conservative:如果儲存格式是地址,就認為是。C/C++有用到這種演算法。
- Compiler hints:對於靜態語言,比如Java,編譯器是知道它是不是指標的,所以可以用這種。
- 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()
裡面的 a
和 b
在函式執行完畢後,就不能通過外面的上下文進行訪問了,所以就可以清除了。
下面,我們簡述下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色標記:黑、白、灰。步驟如下:
- GC開始,所以物件標記為白色。
- 根物件標記為黑色,並開始遍歷其子節點(引用的物件)。
- 當前被遍歷的節點,標記為灰色,被放入一個叫 marking bitmap 的棧。在棧中,把當前被遍歷的節點,標記為黑色,並出棧,同時,把它的子節點(如果有的話)標記為灰色,並壓入棧。(大物件比較特殊,這裡不展開)
- 當所有物件被遍歷完後,就只剩下黑和白。通過Sweeping或Compacting的方式,清理掉白色,完成GC。
小補充:JavaScript的根物件
GC的時候,從根物件開始遍歷。在瀏覽器,根物件是 window
;在 Node.js 中,是 global
(或稱為root
).
Node.js中,每個檔案被當做一個模組,所以,當你用 var/let/const
在檔案的全域性,宣告變數的時候,作用域是當前檔案(模組)。因此,圖中 root.a
是 undefined
.