JS垃圾回收機制筆記

直到不久以前,對於JS的垃圾回收機制,還停留在‘所分配的內存再也不須要’的階段。問題來了,瀏覽器是怎麼肯定‘所分配的內存再也不須要’了呢?javascript

  • 內存簡介
  • 垃圾回收簡介

內存簡介

MDN:像C語言這樣的高級語言通常都有底層的內存管理接口,好比 malloc()和free()。另外一方面,JavaScript建立變量(對象,字符串等)時分配內存,而且在再也不使用它們時「自動」釋放。 後一個過程稱爲垃圾回收。這個「自動」是混亂的根源,並讓JavaScript(和其餘高級語言)開發者感受他們能夠不關心內存管理。 這是錯誤的。java

內存生命週期

  1. 分配你所須要的內存
  2. 使用分配到的內存(讀、寫)
  3. 不須要時將其釋放\歸還

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)。

  1. 標記階段,垃圾回收器會從根對象開始遍歷。每個能夠從根對象訪問到的對象都會被添加一個標識,因而這個對象就被標識爲可到達對象。
  2. 清除階段,垃圾回收器會對堆內存從頭至尾進行線性遍歷,若是發現有對象沒有被標識爲可到達對象,那麼就將此對象佔用的內存回收,而且將原來標記爲可到達對象的標識清除,以便進行下一次垃圾回收操做。

簡單看看下面兩張圖片

  • 在標記階段,從根對象1能夠訪問到B,從B又能夠訪問到E,那麼B和E都是可到達對象,一樣的道理,F、G、J和K都是可到達對象。
  • 在回收階段,全部未標記爲可到達的對象都會被垃圾回收器回收。

這個算法比前一個要好,由於「有零引用的對象」老是不可得到的,可是相反卻不必定,參考「循環引用」。

從2012年起,全部現代瀏覽器都使用了標記-清除垃圾回收算法。全部對JavaScript垃圾回收算法的改進都是基於標記-清除算法的改進,並無改進標記-清除算法自己和它對「對象是否再也不須要」的簡化定義。

什麼時候開始垃圾回收

一般來講,在使用標記清除算法時,未引用對象並不會被當即回收。取而代之的作法是,垃圾對象將一直累計到內存耗盡爲止。當內存耗盡時,程序將會被掛起,垃圾回收開始執行。

標記-清楚算法缺陷

  • 那些沒法從根對象查詢到的對象都將被清除
  • 垃圾收集後有可能會形成大量的內存碎片,像上面的圖片所示,垃圾收集後內存中存在三個內存碎片,假設一個方格表明1個單位的內存,若是有一個對象須要佔用3個內存單位的話,那麼就會致使Mutator一直處於暫停狀態,而Collector一直在嘗試進行垃圾收集,直到Out of Memory。

ChromeV8垃圾回收算法分代回收(Generation GC)

這個和 Java 回收策略思想是一致的。目的是經過區分「臨時」與「持久」對象;多回收「臨時對象區」(young generation),少回收「持久對象區」(tenured generation),減小每次需遍歷的對象,從而減小每次GC的耗時。Chrome 瀏覽器所使用的 V8 引擎就是採用的分代回收策略。

「臨時」與「持久」對象也被叫作做「新生代」與「老生代」對象

V8分代回收

V8內存限制

在node中javascript能使用的內存是有限制的.

  1. 64位系統下約爲1.4GB。
  2. 32位系統下約爲0.7GB。

對應到分代內存中,默認狀況下。

  1. 32位系統新生代內存大小爲16MB,老生代內存大小爲700MB。
  2. 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初始化時生效,一旦生效就不能再動態改變。

V8爲何會有內存限制

  • 表面上的緣由是V8最初是做爲瀏覽器的JavaScript引擎而設計,不太可能遇到大量內存的場景。
  • 而深層次的緣由則是因爲V8的垃圾回收機制的限制。因爲V8須要保證JavaScript應用邏輯與垃圾回收器所看到的不同,V8在執行垃圾回收時會阻塞JavaScript應用邏輯,直到垃圾回收結束再從新執行JavaScript應用邏輯,這種行爲被稱爲「全停頓」(stop-the-world)。
  • 若V8的堆內存爲1.5GB,V8作一次小的垃圾回收須要50ms以上,作一次非增量式的垃圾回收甚至要1秒以上。
  • 這樣瀏覽器將在1s內失去對用戶的響應,形成假死現象。若是有動畫效果的話,動畫的展示也將顯著受到影響。

V8新生代算法(Scavenge)

新生代中的對象主要經過Scavenge算法進行垃圾回收。在Scavenge的具體實現中,主要採用Cheney算法。

  • Cheney算法是一種採用複製的方式實現的垃圾回收算法,它將堆內存一分爲二,這兩個空間中只有一個處於使用中,一個處於閒置狀態。
  • 處於使用狀態的空間稱爲From空間,處於閒置的空間稱爲To空間。
  • 分配對象時,先是在From空間中進行分配,當開始垃圾回收時,會檢查From空間中的存活對象,並將這些存活對象複製到To空間中,而非存活對象佔用的空間被釋放。
  • 完成複製後,From空間和To空間的角色互換。
  • 簡而言之,垃圾回收過程當中,就是經過將存活對象在兩個空間中進行復制。
    Scavenge算法的缺點是隻能使用堆內存中的一半,但因爲它只複製存活的對象,對於生命週期短的場景存活對象只佔少部分,因此在時間效率上有着優異的表現。

晉升

以上所說的是在純Scavenge算法中,可是在分代式垃圾回收的前提下,From空間中存活的對象在複製到To空間以前須要進行檢查,在必定條件下,須要將存活週期較長的對象移動到老生代中,這個過程稱爲對象晉升。

對象晉升的條件有兩個,一種是對象是否經歷過Scacenge回收:

另一種狀況是當To空間的使用應超過25%時,則這個對象直接晉升到老生代空間中。

V8老生代算法(Mark-Sweep,Mark-Compact)

在老生代中的對象,因爲存活對象佔比較大,再採用Scavenge方式會有兩個問題:

  • 一個是存活對象就較多,複製存活對象的效率將會下降;
  • 另外一個依然是浪費一半空間的問題。爲此,V8在老生代中主要採用Mark-Sweep和Mark-Compact相結合的方式進行垃圾回收。

Mark-Sweep(標記- 清除算法)

這個算法上文有提到過,這裏再說一下。

  • 與Scavenge不一樣,Mark-Sweep並不會將內存分爲兩份,因此不存在浪費一半空間的行爲。Mark-Sweep在標記階段遍歷堆內存中的全部對象,並標記活着的對象,在隨後的清除階段,只清除沒有被標記的對象。
  • 也就是說,Scavenge只複製活着的對象,而Mark-Sweep只清除死了的對象。活對象在新生代中只佔較少部分,死對象在老生代中只佔較少部分,這就是兩種回收方式都能高效處理的緣由。
  • 可是這個算法有個比較大的問題是,內存碎片太多。若是出現須要分配一個大內存的狀況,因爲剩餘的碎片空間不足以完成這次分配,就會提早觸發垃圾回收,而此次回收是沒必要要的。
  • 因此在此基礎上提出Mark-Compact算法。

Mark-Compact(標記-整理算法)

Mark-Compact在標記完存活對象之後,會將活着的對象向內存空間的一端移動,移動完成後,直接清理掉邊界外的全部內存。


內存問題

  1. 如今的chrome瀏覽器是否還會存在內存泄漏?
  2. 內存泄漏跟內存溢出的區別是什麼?
  3. chrome什麼時候開始內存回收?
  4. 回收分配的內存必定比不回收要好嗎?

ps: 請勿轉載,只學習交流使用。

相關文章
相關標籤/搜索