JavaScript 中的垃圾回收

weixin_33912246發表於2018-04-17
記憶體的生命週期
javascript 的記憶體分配
javascript 垃圾回收的方法和方式
哪些操作會造成記憶體洩漏

記憶體的生命週期

1 分配所需要的記憶體
2 使用分配到的記憶體進行讀寫操作
3 不需要時將記憶體進行清除

javascript 的記憶體分配

  • 變數初始化分配
var str = 'string' // 為字串分配記憶體
var arr = [1, 2] // 為陣列及數值分配記憶體
var obj = { // 為物件及承載的數值分配記憶體
  a: 1
}
function fn(a, b) { // 為可呼叫的函式變數 fn 物件分配記憶體
  return a + b
}
el.addEventListener('click', function() { // 函式表示式, 匿名函式分配記憶體
  el.style.color = 'red'  
})
  • 呼叫函式分配
var d = new Date(); // 為Date 物件值分配記憶體

var e = document.createElement('div'); // 為 DOM 物件分配記憶體

javascript 垃圾回收的方法

  • 引用計數
  • 標記清除(常用)

引用計數

引用計數垃圾回收演算法

var o = { 
  a: {
    b:2
  }
}; 
// 兩個物件被建立
// { b: 2 } 作為一個屬性被引用  +1 = 1
// { a: { b: 2 } } 被分配給變數 o +1 = 1

var o2 = o; // o2變數是第二個對 { a: { b: 2 } } +1 = 2 的引用 

o = 1;      // 現在,{ a: { b: 2 } } 的原始引用o被o2替換了 -1 = 1

var oa = o2.a; // 引用 { a: { b: 2 } } + 1 = 2的a屬性 { b: 2 } + 1 = 2
// 現在,{ a: { b: 2 } }  有兩個引用了,一個是o2,一個是oa

o2 = "yo"; // { a: { b: 2 } } = 0 物件的原始引用被清除
 // 然而它的屬性a的物件還在被oa引用,所以還不能回收

oa = null; // a屬性 { b: 2 } 現在也是零引用了
// { a: { b: 2 } } 它可以被垃圾回收了

迴圈引用

function f(){
  var o = {}; + 1
  var o2 = {}; + 1
  o.a = o2; // o 引用 o2 + 1
  o2.a = o; // o2 引用 o + 1
  return "str";
}
f()
// var o = {}; var o2 = {}; 在棧中執行後該被清除 
// o.a, o2.a 都至少引用了一次 o 和 o2 無法被清除
var el;
window.onload = function(){
  el = document.getElementById("element");
  el.circularReference = el;
  el.lotsOfData = new Array(10000).join("*");
};
// 當element 元素被刪除後應該被回收
// el.circularReference 迴圈引用了 el, 導致對此 dom 元素的引用無法被回收, el.lotsOfData 的資料無法釋放

標記清除(常用)

在全域性環境或函式環境宣告變數時,進入執行環境,�垃圾回收器將其標記為'進入環境',當變數離開環境、函式執行結束後將其標記為'離開環境'。垃圾收集器會在執行時通過給儲存在記憶體中的所有變數加上標記的方式決定是否應該清除,閉包只有'進入環境'標記。垃圾收集器執行時會對標記為'離開環境'的變數和全域性環境無法訪問到的物件進行清除。

標記清除的迴圈引用

// 函式內宣告的 o 和 o2 因為在全域性環境下無法訪問會被清除
function f(){
  var o = {}; + 1
  var o2 = {}; + 1
  o.a = o2; // o 引用 o2 + 1
  o2.a = o; // o2 引用 o + 1
  return "str";
}
f()
// 當 element 被刪除後或手動取消引用時,全域性環境 el 變數為null,dom 物件佔用的記憶體則被清除
var el;
window.onload = function(){
  el = document.getElementById("element");
  el.circularReference = el;
  el.lotsOfData = new Array(10000).join("*");
};
el = null // 全域性環境無法訪問到el.circularReference 被清除

哪些操作會造成記憶體洩漏

settimeout的第一個引數使用字串而非函式的話,會引發記憶體洩漏。意外的全域性變數、閉包、控制檯日誌、遺留的定時器、在兩個物件彼此引用且彼此保留
解決方法:
函式執行後手動設定 dom 為null, 手動 clear 定時器,避免迴圈引用。

WeakMap

WeakMap 作用

WeakMap WeakSet對於值的引用都是不計入垃圾回收機制的,表示這是弱引用。
先新建一個 Weakmap 例項。然後,將一個 DOM 節點作為鍵名存入該例項,並將一些附加資訊作為鍵值,一起存放在 WeakMap 裡面。這時,WeakMap 裡面對element的引用就是弱引用,不會被計入垃圾回收機制。
當我們想為物件新增資料但是又不想干擾垃圾回收機制就可以使用

const wm = new WeakMap();

const element = document.getElementById('example'); // 引用計數 + 1

wm.set(element, 'some information'); // 弱引用 - 引用計數不變
wm.get(element) // "some information" value 可以為物件

WeakMap 示例
called 大於10後 進行 report 上報 mapobj 引數的引用仍然存在,造成了記憶體洩漏,而我們只是為obj新增了一些額外資訊

var map = new Map(); // maps can have object keys
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // 應該手動清除 map 對 obj 的引用
    map.set(obj, called);
}

使用WeakMap用於處理為物件新增資訊的場景

var map = new WeakMap(); // create a weak map
function useObj(obj){
    doSomethingWith(obj);
    var called = map.get(obj) || 0;
    called++; // called one more time
    if(called > 10) report(); // 無需清除引用
    map.set(obj, called);
}

相關文章