JavaScript中的垃圾回收和記憶體洩漏

薄荷前端發表於2019-03-03

之前接觸的js的記憶體管理方面的內容一直比較零散,最近在這一塊做了一些系統的學習.學習過程中的一些總結在這裡分享給大家.歡迎批評指正,共同學習,共同進步.

在一部分語言中是提供了記憶體管理的介面的,例如C語言中的 malloc()free(); 而在 JavaScript 中會自動進行記憶體的分配和回收的,因為自動這兩個字,就讓很多的開發者認為我們是不需要去關心記憶體方面的問題,當然,這是一種錯誤的看法.關注記憶體的管理,避免記憶體的洩漏也是效能優化重要的一項.

變數的生命週期

Javascript 變數的生命週期要分開來看,對於全域性變數,他的生命週期會持續到頁面關閉(這就涉及到了後面要總結的記憶體洩漏的一種方式).而對於區域性變數,在所在的函式的程式碼執行之後,區域性變數的生命週期結束,他所佔用的記憶體會通過垃圾回收機制釋放(即垃圾回收).

垃圾回收機制

垃圾回收通常有兩種方式來實現:

引用計數

這種演算法在MDN文件中被稱為最"天真"的垃圾回收演算法;核心原理是: 判斷一個物件是否要被回收就是要看是否還有引用指向它,如果是"零引用",那麼就回收.說這種演算法天真,是因為它存在著較為嚴重的缺陷---迴圈引用:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

複製程式碼

首先要注意我們是在函式作用域中討論的這個問題,而不是全域性環境中.老版本的IE中的非JavaScript原生物件如 DOMBOM 物件就採用的這種策略.下面這種情況下就會出現記憶體洩漏:

var el =document.getElementById("some_element");
var Obj =new Object();
myObj.el = el;
el.someObject = Obj;

複製程式碼

當然我們可以在不用的時候手動釋放:

myObj.el = null;
el.someObject = null;

複製程式碼

標記清除

這個演算法把“物件是否不再需要”簡化定義為“物件是否可以獲得”.

這個演算法假定有一個根(root)的物件;在 Javascript 裡,根是全域性物件,對應於瀏覽器環境的 window,node 環境的 global.垃圾回收器將定期從根開始,找所有從根開始引用的物件,然後找這些物件引用的物件……從根開始,垃圾回收器將找到所有可以獲得的物件和收集所有不能獲得的物件.

這個演算法相對於引用計數的優勢在於,“有零引用的物件”總是不可獲得的,但是相反卻不一定,參考“迴圈引用”.

從2012年起,所有現代瀏覽器都使用了標記-清除垃圾回收演算法,都是在此基礎上進行優化.所有對JavaScript垃圾回收演算法的改進都是基於標記-清除演算法的改進,並沒有改進標記-清除演算法本身和它對“物件是否不再需要”的簡化定義.

限制: 那些無法從根物件查詢到的物件都將被清除 當然,在我們的開發實踐中很少遇到這種情況,這也是我們忽略記憶體管理的原因之一.

常見的記憶體洩漏舉例

1.忘記宣告的區域性變數

function a(){
    b=2
    console.log('b沒有被宣告!')
}
複製程式碼

b 沒被宣告,會變成一個全域性變數,在頁面關閉之前不會被釋放.使用嚴格模式可以避免.

2.閉包帶來的記憶體洩漏

var leaks = (function(){
    var leak = 'xxxxxx';// 閉包中引用,不會被回收
    return function(){
        console.log(leak);
    }
})()

複製程式碼

當然有時候我們是故意讓這個變數儲存在記憶體中的,但是要避免無意的時候造成的記憶體洩漏.

3.移除 DOM 節點時候忘記移除暫存的值

有時候出於優化效能的目的,我們會用一個變數暫存 節點,接下來使用的時候就不用再從 DOM 中去獲取.但是在移除 DOM 節點的時候卻忘記了解除暫存的變數對 DOM 節點的引用,也會造成記憶體洩漏

var element = {
  image: document.getElementById('image'),
  button: document.getElementById('button')
};

document.body.removeChild(document.getElementById('image'));
// 如果element沒有被回收,這裡移除了 image 節點也是沒用的,image 節點依然留存在記憶體中.

複製程式碼

與此類似情景還有: DOM 節點繫結了事件, 但是在移除的時候沒有解除事件繫結,那麼僅僅移除 DOM 節點也是沒用的

4. 定時器中的記憶體洩漏

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
複製程式碼

如果沒有清除定時器,那麼 someResource 就不會被釋放,如果剛好它又佔用了較大記憶體,就會引發效能問題. 再提一下 setTimeout ,它計時結束後它的回撥裡面引用的物件佔用的記憶體是可以被回收的. 當然有些場景 setTimeout 的計時可能很長, 這樣的情況下也是需要納入考慮的.

chrome中檢視

老版本的在 Timeline 中檢視, 新版本的在 performance 中檢視:

image

步驟:

  1. 開啟開發者工具 Performance
  2. 勾選 Screenshotsmemory
  3. 左上角小圓點開始錄製(record)
  4. 停止錄製

圖中 Heap 對應的部分就可以看到記憶體在週期性的回落也可以看到垃圾回收的週期,如果垃圾回收之後的最低值(我們稱為min),min在不斷上漲,那麼肯定是有較為嚴重的記憶體洩漏問題.

關於工具的使用暫時在這裡淺嘗輒止了,後面再深入的學習了開發者工具方方面面的使用再來和大家分享.

參考文件:

  1. MDN文件
  2. 推薦給大家的一個ppt

廣而告之

本文釋出於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。

歡迎討論,點個贊再走吧 。◕‿◕。 ~

相關文章