一文徹底搞懂js垃圾回收和記憶體洩露

無悔 問心發表於2020-11-30

一文徹底搞懂js垃圾回收和記憶體洩露

垃圾回收機制

瀏覽器的 Javascript 具有自動垃圾回收機制(GC:Garbage Collecation),也就是說,執行環境會負責管理程式碼執行過程中使用的記憶體。其原理是:垃圾收集器會定期(週期性)找出那些不在繼續使用的變數,然後釋放其記憶體。但是這個過程不是實時的,因為其開銷比較大並且GC時停止響應其他操作,所以垃圾回收器會按照固定的時間間隔週期性的執行。

只有函式內的變數才可能被回收

不再使用的變數也就是生命週期結束的變數,當然只可能是區域性變數,全域性變數的生命週期直至瀏覽器解除安裝頁面才會結束。區域性變數只在函式的執行過程中存在,而在這個過程中會為區域性變數在棧或堆上分配相應的空間,以儲存它們的值,然後在函式中使用這些變數,直至函式結束,而閉包中由於內部函式的原因,外部函式並不能算是結束。
還是上程式碼說明吧:

function fn1() {
    var obj = {name: 'hanzichi', age: 10};
}
function fn2() {
    var obj = {name:'hanzichi', age: 10};
    return obj;
}

var a = fn1();
var b = fn2();

分析:
我們來看程式碼是如何執行的。首先宣告瞭兩個函式,分別叫做 fn1 和 fn2,當 fn1 被呼叫時,進入 fn1 的環境,會開闢一塊記憶體存放物件{name: ‘hanzichi’, age: 10},而當呼叫結束後,出了fn1的環境,那麼該塊記憶體會被 JS 引擎中的垃圾回收器自動釋放;在 fn2 被呼叫的過程中,返回的物件被全域性變數 b 所指向,所以該塊記憶體並不會被釋放。
到底哪個變數是沒有用的?
垃圾收集器必須跟蹤到底哪個變數沒用,對於不再有用的變數打上標記,以備將來收回其記憶體。通常情況下有兩種實現方式:標記清除和引用計數。引用計數不太常用,標記清除較為常用。

標記清除 (因為常用,所以先介紹)

js中最常用的垃圾回收方式就是標記清除。當變數進入環境時,例如,在函式中宣告一個變數,就將這個變數標記為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變數所佔用的記憶體,因為只要執行流進入相應的環境,就可能會用到它們。而當變數離開環境時,則將其標記為“離開環境”。
這段需要背,面試會問到的

function test(){
var a = 10 ;       // 被標記 ,進入環境 
var b = 20 ;       // 被標記 ,進入環境
}
test();            // 執行完畢 之後 a、b又被標離開環境,被回收。

垃圾回收器在執行的時候會給儲存在記憶體中的所有變數都加上標記(當然,可以使用任何標記方式)。然後,它會去掉環境中的變數以及被環境中的變數引用的變數的標記(閉包)。而在此之後再被加上標記的變數將被視為準備刪除的變數,原因是環境中的變數已經無法訪問到這些變數了。最後,垃圾回收器完成記憶體清除工作,銷燬那些帶標記的值並回收它們所佔用的記憶體空間。
到目前為止,IE9+、Firefox、Opera、Chrome、Safari 的 JS 實現使用的都是標記清除的垃圾回收策略,只不過垃圾收集的時間間隔互不相同。

引用計數

引用計數的含義是跟蹤記錄每個值被引用的次數。當宣告瞭一個變數並將一個引用型別值賦給該變數時,則這個值的引用次數就是1。如果同一個值又被賦給另一個變數,則該值的引用次數加 1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數減 1。當這個值的引用次數變成 0 時,則說明沒有辦法再訪問這個值了,因而就可以將其佔用的記憶體空間回收回來。這樣,當垃圾回收器下次再執行時,它就會釋放那些引用次數為 0 的值所佔用的記憶體。
這段需要背,面試會問到的
function test() {
    var a = {};    // a指向物件的引用次數為1
    var b = a;     // a指向物件的引用次數加1,為2
    var c = a;     // a指向物件的引用次數再加1,為3
    var b = {};    // a指向物件的引用次數減1,為2
}

什麼是記憶體洩漏?

程式的執行需要記憶體。只要程式提出要求,作業系統或者執行時(runtime)就必須供給記憶體。
對於持續執行的服務程式(daemon),必須及時釋放不再用到的記憶體。否則,記憶體佔用越來越高,輕則影響系統效能,重則導致程式崩潰。

在這裡插入圖片描述

不再用到的記憶體,沒有及時釋放,就叫做記憶體洩漏(memory leak)。
面試要說的

Chrome 瀏覽器檢視記憶體洩漏

如果連續五次垃圾回收之後,記憶體佔用一次比一次大,就有記憶體洩漏。這就要求實時檢視記憶體佔用。
Chrome自帶的記憶體除錯工具可以很方便地檢視記憶體使用情況和記憶體洩露:
在 Timeline -> Memory 點選record即可:

在這裡插入圖片描述

  • 開啟開發者工具,選擇 Timeline 皮膚
  • 在頂部的Capture欄位裡面勾選 Memory
  • 點選左上角的錄製按鈕。
  • 在頁面上進行各種操作,模擬使用者的使用情況。
  • 一段時間後,點選對話方塊的 stop 按鈕,皮膚上就會顯示這段時間的記憶體佔用情況。

Vue 中的記憶體洩漏問題

1. 如果在mounted/created鉤子中使用 JS 繫結了DOM/BOM物件中的事件,需要在 beforeDestroy中做對應解綁處理;
2.如果在 mounted/created鉤子中使用了第三方庫初始化,需要在 beforeDestroy中 ###### 3.做對應銷燬處理(一般用不到,因為很多時候都是直接全域性 Vue.use);

如果元件中使用了 setInterval,需要在 beforeDestroy中做對應銷燬處理;


mounted() {
    const box = document.getElementById('time-line')
    this.width = box.offsetWidth
    this.resizefun = () => {
      this.width = box.offsetWidth
    }
    window.addEventListener('resize', this.resizefun)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resizefun)
    this.resizefun = null
  }
  

js中的記憶體洩漏

1. 迴圈引用
一個很簡單的例子:一個DOM物件被一個Javascript物件引用,與此同時又引用同一個或其它的Javascript物件,這個DOM物件可能會引發記憶體洩露。這個DOM物件的引用將不會在指令碼停止的時候被垃圾回收器回收。要想破壞迴圈引用,引用DOM元素的物件或DOM物件的引用需要被賦值為null。
2. 閉包

####### 在閉包中引入閉包外部的變數時,當閉包結束時此物件無法被垃圾回收(GC)。


var a = function() {
  var largeStr = new Array(1000000).join('x');
  return function() {
    return largeStr;
  }
}();

3. DOM洩露
當原有的COM被移除時,子結點引用沒有被移除則無法回收。

var select = document.querySelector;
var treeRef = select('#tree');
 
//在COM樹中leafRef是treeFre的一個子結點
var leafRef = select('#leaf'); 
var body = select('body');
 
body.removeChild(treeRef);
 
//#tree不能被回收入,因為treeRef還在
//解決方法:
treeRef = null;
 
//tree還不能被回收,因為葉子結果leafRef還在
leafRef = null;
 
//現在#tree可以被釋放了。

4. Timers計(定)時器洩露
定時器也是常見產生記憶體洩露的地方


for (var i = 0; i < 90000; i++) {
  var buggyObject = {
    callAgain: function() {
      var ref = this;
      var val = setTimeout(function() {
        ref.callAgain();
      }, 90000);
    }
  }
 
  buggyObject.callAgain();
  //雖然你想回收但是timer還在
  buggyObject = null;
}

相關文章