關於內存泄漏

概述

像 C 語言,擁有底層原始的內存管理方法,例如:malloc() 和 free()。這些原始的方法被開發者用來從操做系統中分配內存和釋放內存。javascript

然而,JavaScript 當一些東西(objects,strings,etc.)被建立的時候分配內存而且當它們再也不被使用的時候「自動」釋放它們,這個過程被稱爲垃圾回收。java

釋放資源的這種看似「自動」的性質是形成困擾的根源。它給 JavaScript (和其它高級語言)開發者一個錯誤的印象——他們能夠選擇不關心內存管理。這是一個很大的錯誤。算法

即便是使用高級語言,開發者對內存管理也應該有所瞭解(至少要有基礎的瞭解)。有時,開發者必須理解自動內存管理會遇到問題(例如:垃圾回收中的錯誤或者性能問題等),以便可以正確處理它們。(或者是找到適當的解決方法,用最小的代價去解決。)編程

 

關於內存數組

 

不少東西都存儲在內存中:瀏覽器

 

  1. 全部程序使用的全部變量和其餘數據。
  2. 程序的代碼,包括操做系統的。

編譯代碼時,編譯器能夠檢查原始數據類型,並提早計算出須要多少內存。而後將所需的數量分配給調用堆棧中的程序。這些變量分配的空間稱爲堆棧空間,由於隨着函數被調用,它們的內存被添加到現有存儲器的頂部。當它們終止時,它們以 LIFO (last-in,first-out)順序被移除。靜態和動態分配內存數據結構

它不能爲堆棧上的變量分配空間。相反,咱們的程序須要在運行時明確地要求操做系統得到適當的空間量。這個內存時從堆空間分配的。靜態和動態內存分配的區別以下表所示:閉包

Static allocation Dynamic allocation
編譯時內存大小肯定 編譯時內存大小不肯定
編譯階段執行 運行時執行
分配給棧 分配給堆
FILO 沒有特定的順序

JavaScript 中使用內存

基本上在 JavaScript 中使用內存的意思就是在內存在進行 讀 和 寫。框架

這個操做多是一個變量值的讀取或寫入,一個對象屬性的讀取或寫入,甚至時向函數中傳遞參數。dom

什麼是內存泄漏

實質上,內存泄漏能夠被定義爲應用程序再也不須要的內存,但因爲某種緣由,內存不會返回到操做系統或可用內存池中。

編程語言支持多種管理內存的方法。然而,某塊內存是否被使用其實是一個不肯定的問題。換句話說,只有開發人員能夠清楚一塊內存是否能夠釋放到操做系統又或者不應被釋放。

某些編程語言提供了一些特性,幫助開發者處理這些事情。另一些語言指望開發者可以徹底本身去明確地控制內存。維基百科有關於手動和自動內存管理的好文章。

四種常見的 JavaScript 內存泄漏

1:Global variables

JavaScript 以有趣的方式處理未聲明的變量:對未聲明的變量的引用在全局對象內建立一個新變量。在瀏覽器中,全局對象就是 window。換種說法:

  1.  
    function foo(arg) {
  2.  
    bar = "some text";
  3.  
    }

等價於:

  1.  
    function foo(arg) {
  2.  
    window.bar = "some text";
  3.  
    }

若是 bar 被假定爲僅僅在函數 foo 的做用域範圍內持有對變量的引用,可是你卻忘記了使用 var 來聲明它,那麼就會建立一個意外的全局變量。

在這個例子中,泄漏一個簡單的字符串不會有太大的傷害,但確定會變得更糟的。

能夠經過另外一種方式建立意外的全局變量:

  1.  
    function foo() {
  2.  
    this.var1 = "potential accidental global";
  3.  
    }
  4.  
    // Foo called on its own, this points to the global object (window)
  5.  
    // rather than being undefined.
  6.  
    foo();

爲了防止這些錯誤的發生,能夠在 JavaScript 文件開頭添加 「use strict」,使用嚴格模式。這樣在嚴格模式下解析 JavaScript 能夠防止意外的全局變量。

即便咱們討論瞭如何預防意外全局變量的產生,可是仍然會有不少代碼用顯示的方式去使用全局變量。這些全局變量是沒法進行垃圾回收的(除非將它們賦值爲 null 或從新進行分配)。特別是用來臨時存儲和處理大量信息的全局變量很是值得關注。若是你必須使用全局變量來存儲大量數據,那麼,請確保在使用完以後,對其賦值爲 null 或者從新分配。

2:被忘記的 Timers 或者 callbacks

在 JavaScript 中使用 setInterval 很是常見。

大多數庫都會提供觀察者或者其它工具來處理回調函數,在他們本身的實例變爲不可達時,會讓回調函數也變爲不可達的。對於 setInterval,下面這樣的代碼是很是常見的:

  1.  
    var serverData = loadData();
  2.  
    setInterval( function() {
  3.  
    var renderer = document.getElementById('renderer');
  4.  
    if(renderer) {
  5.  
    renderer.innerHTML = JSON.stringify(serverData);
  6.  
    }
  7.  
    }, 5000); //This will be executed every ~5 seconds.

這個例子闡述着 timers 可能發生的狀況:計時器會引用再也不須要的節點或數據。

renderer 可能在未來會被移除,使得 interval 內的整個塊都再也不被須要。可是,interval handler 由於 interval 的存活,因此沒法被回收(須要中止 interval,才能回收)。若是 interval handler 沒法被回收,則它的依賴也不能被回收。這意味着 serverData——可能存儲了大量數據,也不能被回收。在觀察者模式下,重要的是在他們再也不被須要的時候顯式地去刪除它們(或者讓相關對象變爲不可達)。

過去,特別是某些瀏覽器(IE6)沒法管理循環引用。現在,大多數瀏覽器會在被觀察的對象不可達時對 observer handlers 進行回收,即便 listener 沒有被顯式的移除。可是,明確地刪除這些 observers 仍然是一個很好的作法。例如:

  1.  
    var element = document.getElementById('launch-button');
  2.  
    var counter = 0;
  3.  
    function onClick(event) {
  4.  
    counter++;
  5.  
    element.innerHtml = 'text ' + counter;
  6.  
    }
  7.  
    element.addEventListener('click', onClick);
  8.  
    // Do stuff
  9.  
    element.removeEventListener('click', onClick);
  10.  
    element.parentNode.removeChild(element);
  11.  
    // Now when element goes out of scope,
  12.  
    // both element and onClick will be collected even in old browsers // that don't handle cycles well.

現在,現代瀏覽器(包括 IE 和 Edge)都使用的是現代垃圾回收算法,能夠檢測這些循環依賴並正確的處理它們。換句話說,讓一個節點不可達,能夠沒必要而在調用 removeEventListener。

框架和庫,例如 jQuery ,在處理掉節點以前會刪除 listeners (使用它們特定的 API)。這些由庫的內部進了處理,確保泄漏不會發生。即便是在有問題的瀏覽器下運行,如。。。。IE6。

3:閉包

JavaScript 開發的一個關鍵方面就是閉包:一個能夠訪問外部(封閉)函數變量的內部函數。因爲 JavaScript 運行時的實現細節,能夠經過如下方式泄漏內存:

  1.  
    var theThing = null;
  2.  
    var replaceThing = function () {
  3.  
    var originalThing = theThing;
  4.  
    var unused = function () {
  5.  
    if (originalThing) // a reference to 'originalThing'
  6.  
    console.log("hi");
  7.  
    };
  8.  
    theThing = {
  9.  
    longStr: new Array(1000000).join('*'),
  10.  
    someMethod: function () {
  11.  
    console.log("message");
  12.  
    }
  13.  
    };
  14.  
    };
  15.  
    setInterval(replaceThing, 1000);

這個代碼片斷作了一件事:每次調用 replaceThing 時,theThing 都會得到一個新對象,它包含一個大的數組和一個新的閉包(someMethod)。同時,變量 unused 保留了一個擁有 originalThing 引用的閉包(前一次調用 theThing 賦值給了 originalThing)。已經有點混亂了嗎?重要的是,一旦一個做用域被建立爲閉包,那麼它的父做用域將被共享。

在這個例子中,建立閉包 someMethod 的做用域是於 unused 共享的。unused 擁有 originalThing 的引用。儘管 unused 歷來都沒有使用,可是 someMethod 可以經過 theThing 在 replaceThing 以外的做用域使用(例如全局範圍)。而且因爲 someMethod 和 unused 共享 閉包範圍,unused 的引用將強制保持 originalThing 處於活動狀態(兩個閉包之間共享整個做用域)。這樣防止了垃圾回收。
當這段代碼重複執行時,能夠觀察到內存使用量的穩定增加。當 GC 運行時,也沒有變小。實質上,引擎建立了一個閉包的連接列表(root 就是變量 theThing),而且這些閉包的做用域中每個都有對大數組的間接引用,致使了至關大的內存泄漏,以下圖:

image

這個問題由 Meteor 團隊發現的,他們有一篇偉大的文章,詳細描述了這個問題。

4:DOM 引用

有時候,在數據結構中存儲 DOM 結構是有用的。假設要快速更新表中的幾行內容。將每行 DOM 的引用存儲在字典或數組中多是有意義的。當這種狀況發生時,就會保留同一 DOM 元素的兩份引用:一個在 DOM 樹種,另外一個在字典中。若是未來某個時候你決定要刪除這些行,則須要讓兩個引用都不可達。

  1.  
    var elements = {
  2.  
    button: document.getElementById('button'),
  3.  
    image: document.getElementById('image')
  4.  
    };
  5.  
    function doStuff() {
  6.  
    elements.image.src = 'http://example.com/image_name.png';
  7.  
    }
  8.  
    function removeImage() {
  9.  
    // The image is a direct child of the body element.
  10.  
    document.body.removeChild(document.getElementById('image'));
  11.  
    // At this point, we still have a reference to #button in the
  12.  
    //global elements object. In other words, the button element is
  13.  
    //still in memory and cannot be collected by the GC.
  14.  
    }

還有一個額外的考慮,當涉及 DOM 樹內部或葉子節點的引用時,必須考慮這一點。假設你在 JavaScript 代碼中保留了對 table 特定單元格(<td>)的引用。有一天,你決定從 DOM 中刪除該 table,但扔保留着對該單元格的引用。直觀地來看,能夠假設 GC 將收集除了該單元格以外全部的內容。實際上,這不會發生的:該單元格是該 table 的子節點,而且 children 保持着對它們 parents 的引用。也就是說,在 JavaScript 代碼中對單元格的引用會致使整個表都保留在內存中的。保留 DOM 元素的引用時,須要仔細考慮。

相關文章
相關標籤/搜索