完全掌握js內存泄漏以及如何避免

前言:內存泄漏寫任何語言都必須得注意的問題,我司技術老大平常吐槽:之前作遊戲的改內存泄漏的bug,如今寫前端仍是這些問題。

什麼是內存泄漏

內存泄漏能夠定義爲程序再也不使用或不須要的一塊內存,可是因爲某種緣由沒有被釋放仍然被沒必要要的佔有。在代碼中建立對象和變量會佔用內存,可是javaScript是有本身的內存回收機制,能夠肯定那些變量再也不須要,並將其清除。可是當你的代碼存在邏輯缺陷的時候,你覺得你已經不須要,可是程序中還存在着引用,致使程序運行完後並無合適的回收所佔用的空間,致使內存不斷的佔用,運行的時間越長佔用的就越多,隨之出現的是,性能不佳,高延遲,頻繁崩潰。 在深刻了解內存泄漏以前,咱們須要知道一下幾點:javascript

  1. 內存生命週期
  2. 內存管理系統
  3. 垃圾回收算法

內存生命週期

無論什麼程序語言,內存生命週期基本是一致的:文章封面圖前端

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

全部語言第二部分都是明確的。第一和第三部分在底層語言中是明確的,但在像JavaScript這些高級語言中,大部分都是隱含的。java

內存管理系統:手動or自動

不一樣的語言經過不一樣的方式來處理其內存。node

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

垃圾回收算法

垃圾回收機制經過按期的檢查哪些先前分配的內存「仍然被須要」(×) 垃圾回收機制經過按期的檢查哪些先前分配的內存「程序的其餘部分仍然能夠訪問到的內存」(√)。算法

嗯? 一臉懵逼。。。。數組

這是理解垃圾回收的關鍵,只有開發者知道這一塊內存是否在將來使用「被須要」,可是能夠經過算法來肯定沒法訪問的內存並將其標記返回操做系統。瀏覽器

  • 引用計數
  • 標記清除

1. 引用計數

這是最初級的垃圾收集算法,若是沒有引用指向該對象(零引用),對象將被垃圾回收機制回收。(MDN上面的例子)session

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  foo(){
       var  obj1  = {};
       var  obj2  = {};
       obj1.x  =  obj2 ; // obj1引用obj2
       obj2.x  =  obj1 ; // obj2引用obj1
       return true ;
   }
   foo();
複製代碼

2. 標記清除

算法由如下幾步組成:併發

  1. 垃圾回收器建立了一個「roots」列表。Roots一般是代碼中全局變量的引用。JavaScript中,「window」對象是一個全局變量,被看成root。window對象老是存在,所以垃圾回收器能夠檢查它和它的全部子對象是否存在(即不是垃圾);
  2. 全部的 roots被檢查和標記爲激活(即不是垃圾)。全部的子對象也被遞歸地檢查。從root開始的全部對象若是是可達的,它就不被看成垃圾。
  3. 全部未被標記的內存會被當作垃圾,收集器如今能夠釋放內存,歸還給操做系統了。

無圖言( )

循環引用的問題迎刃而解.雖然這個算法在不停的改進,js垃圾回收的(生成/增量/併發垃圾回收)這些改進的本質上仍是相同的: 可達內存被標記,其他的被看成垃圾回收。

缺點: 算法運行時程序執行被暫停。

垃圾回收機制不可預測

雖然垃圾回收機制很好很方便,可是得本身權衡.緣由是咱們沒法肯定什麼時候會執行收集.只有開發人員才能明確是否能夠將一塊內存返回給操做系統。不須要的引用是指開發者明知內存引用再也不須要,而咱們在寫程序的時候卻因爲某些緣由,它仍被留在激活的 root 樹中。

如何避免

垃圾收集語言泄漏的主要緣由是不須要的引用。要了解不須要的引用是什麼,首先咱們須要瞭解垃圾收集器如何肯定是否能夠訪問一塊內存。

所以,要了解哪些是JavaScript中最多見的泄漏,咱們須要知道引用一般被遺忘的方式

常見的四種內存泄漏

1. 全局變量

在非嚴格模式下當引用未聲明的變量時,會在全局對象中建立一個新變量。在瀏覽器中,全局對象將是window,這意味着

function foo(arg){ 
    bar =「some text」; // bar將泄漏到全局.
}
複製代碼

爲何不能泄漏到全局呢,咱們平時都會定義全局變量呢!!!

** 緣由 ** :全局變量是根據定義沒法被垃圾回收機制收集.須要特別注意用於臨時存儲和處理大量信息的全局變量。若是必須使用全局變量來存儲數據,請確保將其指定爲null或在完成後從新分配它。 ** 解決辦法 **: 嚴格模式

2. 被遺忘的定時器和回調函數

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

緣由:與節點或數據關聯的計時器再也不須要,node 對象能夠刪除,整個回調函數也不須要了。但是,計時器回調函數仍然沒被回收(計時器中止纔會被回收)。同時,someResource 若是存儲了大量的數據,也是沒法被回收的。

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

3. DOM引用

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

緣由: 保留了DOM節點的引用,致使GC沒有回收

解決辦法:refA = null;

注意: 此外還要考慮 DOM 樹內部或子節點的引用問題。假如你的 JavaScript 代碼中保存了表格某一個 <td> 的引用。未來決定刪除整個表格的時候,直覺認爲 GC 會回收除了已保存的 <td> 之外的其它節點。實際狀況並不是如此:此 <td> 是表格的子節點,子元素與父元素是引用關係。因爲代碼保留了 <td> 的引用,致使整個表格仍待在內存中。保存 DOM 元素引用的時候,要當心謹慎。

4. 閉包

注意

注意

注意: 閉包自己沒有錯,不會引發內存泄漏.而是使用錯誤致使.

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);
複製代碼

這是一段糟糕的代碼,每次調用 replaceThing ,theThing 獲得一個包含一個大數組和一個新閉包(someMethod)的新對象。同時,變量 unused 是一個引用 originalThing 的閉包(先前的 replaceThing 又調用了theThing)。思緒混亂了嗎?最重要的事情是,閉包的做用域一旦建立,它們有一樣的父級做用域,做用域是共享的。someMethod 能夠經過 theThing 使用,someMethod 與 unused 分享閉包做用域,儘管 unused 從未使用,它引用的 originalThing 迫使它保留在內存中(防止被回收)。當這段代碼反覆運行,就會看到內存佔用不斷上升,垃圾回收器(GC)並沒有法下降內存佔用。本質上,閉包的鏈表已經建立,每個閉包做用域攜帶一個指向大數組的間接的引用,形成嚴重的內存泄漏。

解決: 去除unuserd函數或者在replaceThing函數最後一行加上 originlThing = null.

參考:

4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them

How JavaScript works: memory management + how to handle 4 common memory leaks

相關文章
相關標籤/搜索