直到不久以前,對於JS的垃圾回收機制,還停留在‘所分配的內存再也不須要’的階段。問題來了,瀏覽器是怎麼肯定‘所分配的內存再也不須要’了呢?javascript
MDN:像C語言這樣的高級語言通常都有底層的內存管理接口,好比 malloc()和free()。另外一方面,JavaScript建立變量(對象,字符串等)時分配內存,而且在再也不使用它們時「自動」釋放。 後一個過程稱爲垃圾回收。這個「自動」是混亂的根源,並讓JavaScript(和其餘高級語言)開發者感受他們能夠不關心內存管理。 這是錯誤的。java
JavaScript內存分配
爲了避免讓程序員費心分配內存,JavaScript 在定義變量時就完成了內存分配。node
var n = 123; // 給數值變量分配內存
var s = "azerty"; // 給字符串分配內存
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 d = new Date(); // 分配一個 Date 對象
var e = document.createElement('div'); // 分配一個 DOM 元素
複製代碼
var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一個新的字符串
// 由於字符串是不變量,
// JavaScript 可能決定不分配內存,
// 只是存儲了 [0-3] 的範圍。
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2);
// 新數組有四個元素,是 a 鏈接 a2 的結果
複製代碼
使用值的過程其實是對分配內存進行讀取與寫入的操做。讀取與寫入多是寫入一個變量或者一個對象的屬性值,甚至傳遞函數的參數。算法
MDN:大多數內存管理的問題都在這個階段。在這裏最艱難的任務是找到「所分配的內存確實已經再也不須要了」。它每每要求開發人員來肯定在程序中哪一塊內存再也不須要而且釋放它。chrome
高級語言解釋器嵌入了「垃圾回收器」,它的主要工做是跟蹤內存的分配和使用,以便當分配的內存再也不使用時,自動釋放它。這隻能是一個近似的過程,由於要知道是否仍然須要某塊內存是沒法斷定的(沒法經過某種算法解決)數組
引用概念
垃圾回收算法主要依賴於引用的概念。瀏覽器
在內存管理的環境中,一個對象若是有訪問另外一個對象的權限(隱式或者顯式),叫作一個對象引用另外一個對象。例如,一個Javascript對象具備對它原型的引用(隱式引用)和對它屬性的引用(顯式引用)。app
「對象」的概念不只特指 JavaScript 對象,還包括函數做用域(或者全局詞法做用域)。函數
這是最初級的垃圾收集算法。此算法把「對象是否再也不須要」簡化定義爲「對象有沒有其餘對象引用到它」。若是沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。
var o = {
a: {
b:2
}
};
// 兩個對象被建立,一個做爲另外一個的屬性被引用,另外一個被分配給變量o
// 很顯然,沒有一個能夠被垃圾收集
var o2 = o; // o2變量是第二個對「這個對象」的引用
o = 1; // 如今,「這個對象」的原始引用o被o2替換了
var oa = o2.a; // 引用「這個對象」的a屬性
// 如今,「這個對象」有兩個引用了,一個是o2,一個是oa
o2 = "yo"; // 最初的對象如今已是零引用了
// 他能夠被垃圾回收了
// 然而它的屬性a的對象還在被oa引用,因此還不能回收
oa = null; // a屬性的那個對象如今也是零引用了
// 它能夠被垃圾回收了
複製代碼
該算法有個限制:沒法處理循環引用。在下面的例子中,兩個對象被建立,並互相引用,造成了一個循環。它們被調用以後會離開函數做用域,因此它們已經沒有用了,能夠被回收了。然而,引用計數算法考慮到它們互相都有至少一次引用,因此它們不會被回收。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
複製代碼
這個算法把「對象是否再也不須要」簡化定義爲「對象是否能夠得到」。
此算法能夠分爲兩個階段,一個是標記階段(mark),一個是清除階段(sweep)。
簡單看看下面兩張圖片
這個算法比前一個要好,由於「有零引用的對象」老是不可得到的,可是相反卻不必定,參考「循環引用」。
從2012年起,全部現代瀏覽器都使用了標記-清除垃圾回收算法。全部對JavaScript垃圾回收算法的改進都是基於標記-清除算法的改進,並無改進標記-清除算法自己和它對「對象是否再也不須要」的簡化定義。
一般來講,在使用標記清除算法時,未引用對象並不會被當即回收。取而代之的作法是,垃圾對象將一直累計到內存耗盡爲止。當內存耗盡時,程序將會被掛起,垃圾回收開始執行。
分代回收(Generation GC)
這個和 Java 回收策略思想是一致的。目的是經過區分「臨時」與「持久」對象;多回收「臨時對象區」(young generation),少回收「持久對象區」(tenured generation),減小每次需遍歷的對象,從而減小每次GC的耗時。Chrome 瀏覽器所使用的 V8 引擎就是採用的分代回收策略。
「臨時」與「持久」對象也被叫作做「新生代」與「老生代」對象
在node中javascript能使用的內存是有限制的.
- 64位系統下約爲1.4GB。
- 32位系統下約爲0.7GB。
對應到分代內存中,默認狀況下。
- 32位系統新生代內存大小爲16MB,老生代內存大小爲700MB。
- 64位系統下,新生代內存大小爲32MB,老生代內存大小爲1.4GB。
新生代平均分紅兩塊相等的內存空間,叫作semispace,每塊內存大小8MB(32位)或16MB(64位)。
這個限制在node啓動的時候能夠經過傳遞--max-old-space-size 和 --max-new-space-size來調整,如:
node --max-old-space-size=1700 app.js //單位爲MB
node --max-new-space-size=1024 app.js //單位爲MB
複製代碼
上述參數在V8初始化時生效,一旦生效就不能再動態改變。
新生代中的對象主要經過Scavenge算法進行垃圾回收。在Scavenge的具體實現中,主要採用Cheney算法。
以上所說的是在純Scavenge算法中,可是在分代式垃圾回收的前提下,From空間中存活的對象在複製到To空間以前須要進行檢查,在必定條件下,須要將存活週期較長的對象移動到老生代中,這個過程稱爲對象晉升。
對象晉升的條件有兩個,一種是對象是否經歷過Scacenge回收:
另一種狀況是當To空間的使用應超過25%時,則這個對象直接晉升到老生代空間中。
在老生代中的對象,因爲存活對象佔比較大,再採用Scavenge方式會有兩個問題:
這個算法上文有提到過,這裏再說一下。
Mark-Compact
算法。Mark-Compact在標記完存活對象之後,會將活着的對象向內存空間的一端移動,移動完成後,直接清理掉邊界外的全部內存。
ps: 請勿轉載,只學習交流使用。