JavaScript進階-常見內存泄露及如何避免
前言
這一章節(jié)給大家介紹的知識點相對比較簡單, 但是卻是非常重要的. 而且也是在面試過程中經(jīng)常會被問到的一部分內容.
通過此次閱讀你可以學習到:
4種常見的內存泄露 內存泄露的識別方法
4種常見的內存泄露
其實在實際開發(fā)中, 我們很容易不經(jīng)意的就寫出內存泄露的代碼, 比如以下幾種情況可能都是你遇到過的.
一、意外的全局變量
未聲明的變量
當我們在一個函數(shù)中給一個變量賦值但是卻沒有聲明它時:
function fn () {
a = "Actually, I'm a global variable"
}
此時變量a相當于是window對象下的一個變量:
function fn () {
window.a = "Actually, I'm a global variable"
}
而之前我們已經(jīng)說了全局變量是很難被垃圾回收器回收的, 所以要是有這種意外的全局變量應該要避免.
使用this創(chuàng)建的變量
還有一種情況是這樣的:
function fn () {
this.a = "Actually, I'm a global variable"
}
fn();
我們知道, 這里this的指向是window, 因此此時創(chuàng)建的a變量也會被掛載到window對象下.
避免此情況的解決辦法是在 JavaScript 文件頭部或者函數(shù)的頂部加上 'use strict', 開啟嚴格模式, 使得this的指向為undefined, 這樣就可以避免了.
?當然如果你必須使用全局變量存儲大量數(shù)據(jù)時,確保用完以后把它設置為 null 或者重新定義。
”
二、被遺忘的計時器或回調函數(shù)
定時器引起
當我們在代碼中使用定時器也有可能會造成內存泄露:
var serverData = loadData()
setInterval(function() {
var renderer = document.getElementById('renderer')
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData)
}
}, 5000)
上面的例子?中, 節(jié)點renderer引用了serverData.在節(jié)點renderer或者數(shù)據(jù)不再需要時,定時器依舊指向這些數(shù)據(jù)。所以哪怕當renderer節(jié)點被移除后,interval 仍舊存活并且垃圾回收器沒辦法回收,它的依賴也沒辦法被回收,除非終止定時器。
對象觀察者
還有一個就是關于觀察者模式的案例:
var btn = document.getElementById('btn');
function onClick (element) {
element.innerHTMl = "I'm innerHTML"
}
btn.addEventListener('click', onClick);
對于上面觀察者的例子,一旦它們不再需要(或者關聯(lián)的對象變成不可達),明確地移除它們非常重要。老的 IE 6 是無法處理循環(huán)引用的。因為老版本的 IE 是無法檢測 DOM 節(jié)點與 JavaScript 代碼之間的循環(huán)引用,會導致內存泄漏。
但是,現(xiàn)代的瀏覽器(包括 IE 和 Microsoft Edge)使用了更先進的垃圾回收算法(標記清除),已經(jīng)可以正確檢測和處理循環(huán)引用了。即回收節(jié)點內存時,不必非要調用 removeEventListener 了。
三、脫離DOM的引用
這種造成內存泄露的原因簡單來說就是:
如果把DOM 存成字典(JSON 鍵值對)或者數(shù)組,此時,同樣的 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.
四、閉包
還有一種情況就是我們之前提到過的閉包. 也就是局部變量銷毀時, 閉包的這種情況.
首先讓我們明確一點:閉包的關鍵就是匿名函數(shù)能夠訪問父級作用域中的變量.
來看一個簡單的例子?:
function fn () {
var a = "I'm a";
return function () {
console.log(a);
};
}
因為變量a被fn()函數(shù)內的匿名函數(shù)所引用, 因此這種變量是不會被回收的.
那就有人問了, 即使這樣會造成什么不妥嗎? 在上面?這個案例中當然看不出有什么. 若是將情況變得復雜一些呢?
var globalVar = null; // 全局變量
var fn = function () {
var originVal = globalVar; // 局部變量
var unused = function () { // 未使用的函數(shù)
if (originVal) {
console.log('call')
}
}
globalVar = {
longStr: new Array(1000000).join('*'),
someThing: function () {
console.log('someThing')
}
}
}
setInterval(fn, 100);
先請你花上一分鐘看看上面的案例, 你會發(fā)現(xiàn):
每次調用 fn函數(shù)的時候都會產生一個新的對象originVal;變量 unused是一個引用了originVal的閉包;unused雖然未被使用, 但是它引用的originVal迫使它留在內存中, 并不會被回收.
解決辦法是: 可以在fn的最底部, 將originVal設置成null.
內存泄露的識別方法
上面?介紹了這么多種可能會造成內存泄露的情況, 那么有沒有什么實際的辦法讓我們看到內存泄露的表現(xiàn)呢?
當然是有的. 現(xiàn)在常用的是以下2種方式:
Chrome瀏覽器的控制臺Performance或MemoryNode提供的process.memoryUsage方法
Chrome瀏覽器的控制臺Performance或Memory
Chrome 瀏覽器查看內存占用,按照以下步驟操作。
在網(wǎng)頁上右鍵, 點擊“檢查”打開控制臺( Mac快捷鍵option+command+i);選擇 Performance面板(很多教材中用的是Timeline面板, 不知道是不是版本的原因);勾選 Memory, 然后點擊左上角的小黑點Record開始錄制;點擊彈窗中的 Stop結束錄制, 面板上就會顯示這段時間的內存占用情況。


如果內存的使用情況一直在做增量, 那么就是內存泄露了:

或者你可以使用我在《記錄一次定時器及閉包問題造成的內存泄漏》中的方法進行檢查.
Node提供的process.memoryUsage方法
另一個就是Node提供的process.memoryUsage方法, 這一塊我用的不是很多, 這里就貼上教材:
console.log(process.memoryUsage());
// { rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772 }
process.memoryUsage返回一個對象,包含了 Node 進程的內存占用信息。該對象包含四個字段,單位是字節(jié),含義如下:
rss(resident set size):所有內存占用,包括指令區(qū)和堆棧。
heapTotal:"堆"占用的內存,包括用到的和沒用到的。
heapUsed:用到的堆的部分。
external:V8 引擎內部的 C++ 對象占用的內存。
判斷內存泄露, 是看heapUsed字段.
總結
總的來說, 常見的內存泄露包括:
意外的全局變量 被遺忘的定時器或回調函數(shù) 脫離DOM的引用 閉包中重復創(chuàng)建的變量
如何避免內存泄露:
注意程序邏輯,避免“死循環(huán)”之類的 減少不必要的全局變量,或者生命周期較長的對象,及時對無用的數(shù)據(jù)進行垃圾回收 避免創(chuàng)建過多的對象 原則:不用了的東西要及時歸還
