JS高程中的垃圾回收機制與常見記憶體洩露的解決方法

OBKoro1發表於2018-07-09

前言

起因是因為想了解閉包的記憶體洩露機制,然後想起《js高階程式設計》中有關於垃圾回收機制的解析,之前沒有很懂,過一年回頭再看就懂了,寫篇部落格與大家分享一下。如果喜歡的話可以點波贊/關注,支援一下。

個人部落格瞭解一下:obkoro1.com


記憶體的生命週期:

  1. 分配你所需要的記憶體:

由於字串、物件等沒有固定的大小,js程式在每次建立字串、物件的時候,程式都會分配記憶體來儲存那個實體

  1. 使用分配到的記憶體做點什麼。

  2. 不需要時將其釋放回歸:

在不需要字串、物件的時候,需要釋放其所佔用的記憶體,否則將會消耗完系統中所有可用的記憶體,造成系統崩潰,這就是垃圾回收機制所存在的意義

所謂的記憶體洩漏指的是:由於疏忽或錯誤造成程式未能釋放那些已經不再使用的記憶體,造成記憶體的浪費。


垃圾回收機制:

在C和C++之類的語言中,需要手動來管理記憶體的,這也是造成許多不必要問題的根源。幸運的是,在編寫js的過程中,記憶體的分配以及記憶體的回收完全實現了自動管理,我們不用操心這種事情。

垃圾收集機制的原理:

垃圾收集器會按照固定的時間間隔,週期性的找出不再繼續使用的變數,然後釋放其佔用的記憶體

什麼叫不再繼續使用的變數?

不再使用的變數也就是生命週期結束的變數,是區域性變數,區域性變數只在函式的執行過程中存在,當函式執行結束,沒有其他引用(閉包),那麼該變數會被標記回收。

全域性變數的生命週期直至瀏覽器解除安裝頁面才會結束,也就是說全域性變數不會被當成垃圾回收

標記清除:當前採用的垃圾收集策略

工作原理:

當變數進入環境時(例如在函式中宣告一個變數),將這個變數標記為“進入環境”,當變數離開環境時,則將其標記為“離開環境”。標記“離開環境”的就回收記憶體。

工作流程:

  1. 垃圾收集器會在執行的時候會給儲存在記憶體中的所有變數都加上標記
  2. 去掉環境中的變數以及被環境中的變數引用的變數的標記。
  3. 那些還存在標記的變數被視為準備刪除的變數
  4. 最後垃圾收集器會執行最後一步記憶體清除的工作,銷燬那些帶標記的值並回收它們所佔用的記憶體空間

到2008年為止,IE、Chorme、Fireofx、Safari、Opera 都使用標記清除式的垃圾收集策略,只不過垃圾收集的時間間隔互有不同。

引用計數略:被廢棄的垃圾收集策略

迴圈引用:跟蹤記錄每個值被引用的技術

在老版本的瀏覽器中(對,又是IE),IE9以下BOM和DOM物件就是使用C++以COM物件的形式實現的。

COM的垃圾收集機制採用的就是引用計數策略,這種機制在出現迴圈引用的時候永遠都釋放不掉記憶體。

    var element = document.getElementById('something');
    var myObject = new Object();
    myObject.element = element; // element屬性指向dom
    element.someThing = myObject; // someThing回指myObject 出現迴圈引用(兩個物件一直互相包含 一直存在計數)。
複製程式碼

解決方式是,當我們不使用它們的時候,手動切斷連結:

     myObject.element = null; 
     element.someThing = null;
複製程式碼

淘汰

IE9把BOM和DOM物件轉為了真正的js物件,避免了使用這種垃圾收集策略,消除了IE9以下常見的記憶體洩漏的主要原因。

IE7以下有一個宣告狼藉的效能問題,大家瞭解一下:

  1. 256個變數,4096個物件(或陣列)字面或者64KB的字串,達到任何一個臨界值會觸發垃圾收集器執行。
  2. 如果一個js指令碼的生命週期一直保有那麼多變數,垃圾收集器會一直頻繁的執行,引發嚴重的效能問題。

IE7已修復這個問題。


哪些情況會引起記憶體洩漏?

雖然有垃圾回收機制,但我們在編寫程式碼的時候,有些情況還是會造成記憶體洩漏,瞭解這些情況,並在編寫程式的時候,注意避免,我們的程式會更具健壯性。

意外的全域性變數:

上文我們提到了全域性變數不會被當成垃圾回收,我們在編碼中有時會出現下面這種情況:

    function foo() {
     this.bar2 = '預設繫結this指向全域性' // 全域性變數=> window.bar2
      bar = '全域性變數'; // 沒有宣告變數 實際上是全域性變數=>window.bar
    }
    foo();
複製程式碼

當我們使用預設繫結,this會指向全域性,this.something也會建立一個全域性變數,這一點可能很多人沒有注意到。

解決方法:在函式內使用嚴格模式or細心一點

    function foo() {
      "use strict"; 
      this.bar2 = "嚴格模式下this指向undefined"; 
      bar = "報錯";
    }
    foo();
複製程式碼

當然我們也可以手動釋放全域性變數的記憶體

    window.bar = undefined
    delete window.bar2
複製程式碼

被遺忘的定時器和回撥函式

不需要setInterval或者setTimeout時,定時器沒有被clear,定時器的回撥函式以及內部依賴的變數都不能被回收,造成記憶體洩漏。

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
        // 定時器也沒有清除
    }
    // node、someResource 儲存了大量資料 無法回收
}, 1000);
複製程式碼

解決方法: 在定時器完成工作的時候,手動清除定時器。

閉包:

閉包可以維持函式內區域性變數,使其得不到釋放,造成記憶體洩漏

    function bindEvent() {
      var obj = document.createElement("XXX");
      var unused = function () {
          console.log(obj,'閉包內引用obj obj不會被釋放');
      };
      // obj = null;
    }
複製程式碼

解決方法:手動解除引用,obj = null

迴圈引用問題

就是IE9以下的迴圈引用問題,上文講過了。

沒有清理DOM元素引用:

    var refA = document.getElementById('refA');
    document.body.removeChild(refA); // dom刪除了
    console.log(refA, "refA");  // 但是還存在引用 能console出整個div 沒有被回收
複製程式碼

不信的話,可以看下這個dom

解決辦法:refA = null;

console儲存大量資料在記憶體中。

過多的console,比如定時器的console會導致瀏覽器卡死。

解決:合理利用console,線上專案儘量少的使用console,當然如果你要發招聘除外。


如何避免記憶體洩漏:

記住一個原則:不用的東西,及時歸還,畢竟你是'借的'嘛

  1. 減少不必要的全域性變數,使用嚴格模式避免意外建立全域性變數。
  2. 在你使用完資料後,及時解除引用(閉包中的變數,dom引用,定時器清除)。
  3. 組織好你的邏輯,避免死迴圈等造成瀏覽器卡頓,崩潰的問題。

關於記憶體洩漏:

  1. 即使是1byte的記憶體,也叫記憶體洩漏,並不一定是導致瀏覽器崩潰、卡頓才能叫做記憶體洩漏。
  2. 一般是堆區記憶體洩漏,棧區不會洩漏。

基本型別的值存在記憶體中,被儲存在棧記憶體中,引用型別的值是物件,儲存在堆記憶體中。所以物件、陣列之類的,才會發生記憶體洩漏

  1. 使用chorme監控記憶體洩漏,可以看一下這篇文章

結語

瞭解了記憶體洩漏的原因以及出現的情況,那麼我們在編碼過程中只要多加註意,就不會發生非常嚴重的記憶體洩漏問題。

PS:目前離職中,有坑位可以介紹一下,base:上海2號線淞虹路。

希望看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。

個人blog and 掘金個人主頁,如需轉載,請放上原文連結並署名。碼字不易,感謝支援!

如果喜歡本文的話,歡迎關注我的訂閱號,漫漫技術路,期待未來共同學習成長。

JS高程中的垃圾回收機制與常見記憶體洩露的解決方法

以上2018.7.7

參考資料:

JS高程4.3垃圾收集

4類 JavaScript 記憶體洩漏及如何避免

JavaScript記憶體洩露及解決方案詳解

4類 JavaScript 記憶體洩漏及如何避免

相關文章