JavaScript進階-常見內存泄露及如何避免

前言

這一章節給你們介紹的知識點相對比較簡單, 可是倒是很是重要的. 並且也是在面試過程當中常常會被問到的一部份內容.前端

經過這次閱讀你能夠學習到:web

  • 4種常見的內存泄露
  • 內存泄露的識別方法

4種常見的內存泄露

其實在實際開發中, 咱們很容易不經意的就寫出內存泄露的代碼, 好比如下幾種狀況可能都是你遇到過的.面試

1、意外的全局變量

未聲明的變量

當咱們在一個函數中給一個變量賦值可是卻沒有聲明它時:算法

function fn () {
    a = "Actually, I'm a global variable"
}
複製代碼

此時變量a至關因而window對象下的一個變量:數組

function fn () {
    window.a = "Actually, I'm a global variable"
}
複製代碼

而以前咱們已經說了全局變量是很難被垃圾回收器回收的, 因此要是有這種意外的全局變量應該要避免.瀏覽器

使用this建立的變量

還有一種狀況是這樣的:閉包

function fn () {
    this.a = "Actually, I'm a global variable"
}
fn();
複製代碼

咱們知道, 這裏this的指向是window, 所以此時建立的a變量也會被掛載到window對象下.編輯器

避免此狀況的解決辦法是在 JavaScript 文件頭部或者函數的頂部加上 'use strict', 開啓嚴格模式, 使得this的指向爲undefined, 這樣就能夠避免了.ide

固然若是你必須使用全局變量存儲大量數據時,確保用完之後把它設置爲 null 或者從新定義。函數

2、被遺忘的計時器或回調函數

定時器引發

當咱們在代碼中使用定時器也有可能會形成內存泄露:

var serverData = loadData()
setInterval(function() {
	var renderer = document.getElementById('renderer')
	if(renderer) {
		renderer.innerHTML = JSON.stringify(serverData)
	}
}, 5000) 
複製代碼

上面的例子🌰中, 節點renderer引用了serverData.在節點renderer或者數據再也不須要時,定時器依舊指向這些數據。因此哪怕當renderer節點被移除後,interval 仍舊存活而且垃圾回收器沒辦法回收,它的依賴也沒辦法被回收,除非終止定時器。

對象觀察者

還有一個就是關於觀察者模式的案例:

var btn = document.getElementById('btn');
function onClick (element) {
    element.innerHTMl = "I'm innerHTML"
}
btn.addEventListener('click', onClick);
複製代碼

對於上面觀察者的例子,一旦它們再也不須要(或者關聯的對象變成不可達),明確地移除它們很是重要。老的 IE 6 是沒法處理循環引用的。由於老版本的 IE 是沒法檢測 DOM 節點與 JavaScript 代碼之間的循環引用,會致使內存泄漏。

可是,現代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法(標記清除),已經能夠正確檢測和處理循環引用了。即回收節點內存時,沒必要非要調用 removeEventListener 了。

3、脫離DOM的引用

這種形成內存泄露的緣由簡單來講就是:

若是把DOM 存成字典(JSON 鍵值對)或者數組,此時,一樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另外一個在字典中。那麼未來須要把兩個引用都清除。

好比下面這個例子:

// 在對象中引用DOM var elements = { btn: document.getElementById('btn') }

function doSomeThing () { elements.btn.click(); }

複製代碼function removeBtn () { // 將body中的btn移除, 也就是移除 DOM樹中的btn document.body.removeChild(document.getElementById('button')); // 可是此時全局變量elements仍是保留了對btn的引用, btn仍是存在於內存中,不能被GC回收 } 複製代碼

上面👆這種狀況, 能夠手動將引用給清除: elements.btn = null.

4、閉包

還有一種狀況就是咱們以前提到過的閉包. 也就是局部變量銷燬時, 閉包的這種狀況.

首先讓咱們明確一點: 閉包的關鍵就是匿名函數可以訪問父級做用域中的變量.

來看一個簡單的例子🌰:

function fn () {
    var a = "I'm a";
    return function () {
        console.log(a);
    };
}
複製代碼

由於變量afn()函數內的匿名函數所引用, 所以這種變量是不會被回收的.

那就有人問了, 即便這樣會形成什麼不妥嗎? 在上面👆這個案例中固然看不出有什麼. 如果將狀況變得複雜一些呢?

var globalVar = null; // 全局變量
var fn = function () {
    var originVal = globalVar; // 局部變量
    var unused = function () { // 未使用的函數
        if (originVal) {
            console.log('call')
        }
    }
    globalVar = {
        longStr: new Array(1000000).join('*'),
        someThing: function () {
            console.log('someThing')
        }
    }
}
setInterval(fn, 100);
複製代碼

先請你花上一分鐘看看上面的案例, 你會發現:

  1. 每次調用 fn函數的時候都會產生一個新的對象 originVal;
  2. 變量 unused是一個引用了 originVal的閉包;
  3. unused雖然未被使用, 可是它引用的 originVal迫使它留在內存中, 並不會被回收.

解決辦法是: 能夠在fn的最底部, 將originVal設置成null.

內存泄露的識別方法

上面👆介紹了這麼多種可能會形成內存泄露的狀況, 那麼有沒有什麼實際的辦法讓咱們看到內存泄露的表現呢?

固然是有的. 如今經常使用的是如下2種方式:

  • Chrome瀏覽器的控制檯 PerformanceMemory
  • Node提供的 process.memoryUsage方法

Chrome瀏覽器的控制檯PerformanceMemory

Chrome 瀏覽器查看內存佔用,按照如下步驟操做。

  1. 在網頁上右鍵, 點擊「檢查」打開控制檯( Mac快捷鍵 option+command+i);
  2. 選擇 Performance面板(不少教材中用的是 Timeline面板, 不知道是否是版本的緣由);
  3. 勾選 Memory, 而後點擊左上角的小黑點 Record開始錄製;
  4. 點擊彈窗中的 Stop結束錄製, 面板上就會顯示這段時間的內存佔用狀況。

若是內存的使用狀況一直在作增量, 那麼就是內存泄露了:

或者你可使用我在《記錄一次定時器及閉包問題形成的內存泄漏》中的方法進行檢查.

Node提供的process.memoryUsage方法

另外一個就是Node提供的process.memoryUsage方法, 這一塊我用的不是不少, 這裏就貼上教材:

console.log(process.memoryUsage());
// { rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772 }
複製代碼

process.memoryUsage返回一個對象,包含了 Node 進程的內存佔用信息。該對象包含四個字段,單位是字節,含義以下:

rss(resident set size):全部內存佔用,包括指令區和堆棧。
heapTotal:"堆"佔用的內存,包括用到的和沒用到的。
heapUsed:用到的堆的部分。
external: V8 引擎內部的 C++ 對象佔用的內存。
複製代碼

判斷內存泄露, 是看heapUsed字段.

總結

總的來講, 常見的內存泄露包括:

  • 意外的全局變量
  • 被遺忘的定時器或回調函數
  • 脫離DOM的引用
  • 閉包中重複建立的變量

如何避免內存泄露:

  • 注意程序邏輯,避免「死循環」之類的
  • 減小沒必要要的全局變量,或者生命週期較長的對象,及時對無用的數據進行垃圾回收
  • 避免建立過多的對象 原則:不用了的東西要及時歸還

後語

關於JavaScript進階內存堆棧的內容就告一段落了, 總共是有五篇文章. 在文章中我也是儘可能用比較通俗的語言來讓進行講解, 但願你們都能搞懂.

若是你們喜歡霖呆呆的文章的話, 還請幫我一個小忙, 關注一波我最近纔開始搗鼓的公衆號, 我會在上面不按期的發一些關於前端方面的原創文章, 一塊兒學習, 一塊兒進步😊.

LinDaiDai公衆號二維碼.jpg
LinDaiDai公衆號二維碼.jpg
相關文章
相關標籤/搜索