簡介
低階語言,例如C,有低階的記憶體管理基元,想malloc(),free()。另一方面,JavaScript的記憶體基元在變數(對象,字串等等)創建時分配,然後在他們不再被使用時「自動」釋放。後者被稱為垃圾回收。這個「自動」是混淆並給JavaScript(和其他高階語言)開發者一個錯覺:他們可以不用考慮記憶體管理。
記憶體生命週期
不管什麼程式語言,記憶體生命週期基本上一致:
1.分配你所需要的記憶體
2.使用它(讀、寫)
3.當它不被使用時釋放 ps:和「把大象裝冰箱「一個意思
第一二部分過程在所有語言中都很清晰。最後一步在低階語言中很清晰,但是在像JavaScript等高階語言中,最後一步不清晰。
JavaScript的記憶體分配
變數初始化
為了不讓程式設計師為分配費心,JavaScript在定義變數時完成記憶體分配。
var o = {
a: 1,
b: null
}; // 為物件及其包含變數分配記憶體
var a = [1, null, "abra"]; // 為陣列及其包含變數分配記憶體(就像物件)
function f(a){
return a 2;
} // 為函數(可呼叫的物件)分配記憶體
// 函數表達式也能分配一個物件
someElement.addEventListener('click', function(){
someElement.style.backgroundColor = 'blue';
}, false);
透過函數呼叫的記憶體分配
有些函數呼叫結果是分配物件記憶體:
有些方法會分配新變數或新物件:
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); // 新數組中有連接數組a和數組a2中的四個元素。
值的使用
使用值的過程實際上是對分配記憶體進行讀取與寫入的操作,這意味著可以寫入一個變數或一個物件的屬性值,甚至是傳遞函數的參數。
當記憶體不再需要使用時釋放
大多數記憶體管理的問題都在這個階段。這裡最艱難的任務是找到「所分配的記憶體確實已經不再需要了」。它往往要求開發人員來確定在程式中哪一塊記憶體不再需要並且釋放它。
高級語言解釋器嵌入了“垃圾回收器”,主要工作是追蹤記憶體的分配和使用,以便當分配的記憶體不再使用時,自動釋放它。這個過程是一個近似的,因為要知道某塊記憶體是否需要是 無法判定的 (無法被某種演算法所解決).
垃圾回收
如上文所述自動尋找是否一些記憶體「不再需要」的問題是無法判定的。因此,垃圾回收實現只能有限制的解決一般問題。本節將解釋必要的概念,以了解主要的垃圾回收演算法和它們的限制。
引用
垃圾回收演算法主要依賴引用的概念。在記憶體管理的環境中,一個物件如果有存取另一個物件的權限(隱式或顯式),叫做一個物件引用另一個物件。例如,一個Javascript物件具有對它 原型 的引用(隱含引用)和對它屬性的引用(明確引用)。
在這裡,「對象」的概念不僅特製Javascript對象,還包括函數作用域(或全域詞法作用域)。
引用計數垃圾收集
這是最簡單的垃圾收集演算法。此演算法把「物件是否不再需要」簡化定義為「物件有沒有其他物件引用到它」。如果沒有引用指向該物件(零引用),物件將被垃圾回收機制回收。
例如
var o2 = o; // o2變數是第二個對「這個物件」的引用
o = 1; // 現在,「這個物件」的原始引用o被o2取代了
var oa = o2.a; // 引用「這個物件」的a屬性
// 現在,「這個物件」有兩個引用了,一個是o2,一個是oa
o2 = "yo"; // 最初的物件現在已經是零引用了
// 他可以被垃圾回收了
// 然而它的屬性a的物件還在被oa引用,所以還不能回收
oa = null; // a屬性的那個物件現在也是零引用了
// 它可以被垃圾回收了
限制:循環引用
這個簡單的演算法有一個限制,就是如果一個物件引用另一個(形成了循環引用),他們可能「不再需要」了,但是他們不會被回收。
return "azerty";
}
f();
// 兩個物件被創建,並互相引用,形成了一個循環
// 當他們被呼叫之後不會離開函數作用域
// 所以他們已經沒有用了,可以回收了
// 然而,引用計數演算法考慮到他們互相都有至少一次引用,所以他們不會被回收
實際當中的例子
IE 6, 7 對DOM物件進行引用計數回收。對他們來說,一個常見問題是記憶體外洩:
標記-清除演算法
這個演算法把「物件是否不再需要」簡化定義為「物件是否可以獲得」。
這個演算法假定設定一個叫做根的物件(在Javascript裡,根是全域物件)。定期的,垃圾回收器將從根開始,找所有從根開始引用的對象,然後找這些對象引用的對象……從根開始,垃圾回收器將找到所有可以獲得的對象和所有不能獲得的對象。
這個演算法比前一個好,因為「有零引用的物件」總是不可獲得的,但是相反卻不一定,參考「循環引用」。
從2012年起,所有現代瀏覽器都使用了標記-清除垃圾回收演算法。所有對JavaScript垃圾回收演算法的改進都是基於標記-清除演算法的改進,並沒有改進標記-清除演算法本身和它對「物件是否不再需要」的簡化定義。
循環引用不再是問題了
在上面的範例中,函數呼叫傳回之後,兩個物件從全域物件出發無法取得。因此,他們將會被垃圾回收器回收。
第二個範例同樣,一旦 div 和其事件處理無法從根部獲取到,他們將會被垃圾回收器回收。
限制: 物件需要明確的不可取得
儘管這是一個限制,但是很少會被突破,這也就是為什麼在現實中很少人會去關心垃圾回收機制。