title: JavaScript中的內(nèi)存泄漏以及如何處理
date: 2017-11-21 21:27:04
tags:
隨著現(xiàn)代編程語言功能越來越成熟钱慢、復(fù)雜,內(nèi)存管理也容易被大家忽略诫咱。本文將討論Javascript中的內(nèi)存泄露以及如何處理莫辨,方便大家在使用JavaScript編碼時(shí),更好的應(yīng)對(duì)內(nèi)存泄露帶來的問題结啼。
概述
當(dāng)創(chuàng)建對(duì)象和字符串等時(shí)掠剑,JavaScript就會(huì)分配內(nèi)存,并在不使用時(shí)自動(dòng)釋放內(nèi)存郊愧,這種機(jī)制被稱為垃圾收集朴译。這種釋放資源看似是“自動(dòng)”的井佑,但本質(zhì)是混淆的,這也給JavaScript的開發(fā)人員產(chǎn)生了可以不關(guān)心內(nèi)存管理的錯(cuò)誤印象眠寿。其實(shí)這是一個(gè)大錯(cuò)誤躬翁。
什么是內(nèi)存泄露
內(nèi)存泄露是應(yīng)用程序使用過的內(nèi)存片段,在不再需要時(shí)澜公,不能返回到操作系統(tǒng)或可用內(nèi)存池中的情況姆另。
編程語言有各自不同的內(nèi)存管理方式。但是是否使用某一段內(nèi)存坟乾,實(shí)際上是一個(gè)不可判定的問題迹辐。換句話說,只有開發(fā)人員明確的知道是否需要將一塊內(nèi)存返回給操作系統(tǒng)甚侣。
四種常見的JavaScript內(nèi)存泄露
1.全局變量
JavaScript以一種有趣的方式來處理未聲明的變量:當(dāng)引用未聲明的變量時(shí)明吩,會(huì)在全局對(duì)象中創(chuàng)建一個(gè)新變量。在瀏覽器中殷费,全局對(duì)象將是window印荔,這意味著
function foo(arg){
bar = "some text";
}
相當(dāng)于:
function foo(arg){
window.bar = "some text";
}
bar只是foo函數(shù)中引用一個(gè)變量。如果你不適用var聲明详羡,將會(huì)創(chuàng)建u多余的全局變量仍律。在上述情況下,不會(huì)造成很大的問題实柠。但是水泉,若是下面的這種情況。你可能不小心創(chuàng)建一個(gè)全局變量this:
function foo(){
this.val1 = "potential accidental global";
}
// Foo called on its own, this points to the global object (window) rather than being undefined.
foo();
你可以通過在JavaScript文件的開始處添加‘use strict’窒盐;來避免這種錯(cuò)誤草则,這種方式將開啟嚴(yán)格的解析JavaScript模式,從而防止意外創(chuàng)建全局變量蟹漓。
意外的全局變量當(dāng)然是一個(gè)問題炕横。更多的時(shí)候,你的代碼會(huì)受到顯示的全局變量的影響葡粒,而這些全局變量在垃圾收集器中是無法收集份殿。需要特別注意用于臨時(shí)存儲(chǔ)和處理大量信息的全局變量。如果必須使用全局變量來存儲(chǔ)數(shù)據(jù)嗽交,那么確保將其分配為空值伯铣,或者在完成后重新分配。
2.被遺忘的定時(shí)器或回調(diào)
下面列舉setInterval的例子轮纫,這也是經(jīng)常在JavaScript中使用。
對(duì)于提供監(jiān)視的庫和其他接收回調(diào)的工具焚鲜,通常在確保所有回調(diào)的引用在其實(shí)例無法訪問時(shí)掌唾,會(huì)變成無法訪問的狀態(tài)放前。但是下面的代碼卻是一個(gè)例外:
var serverData = loadData();
setInterval(function(){
var renderer = document.getElementById('render');
if(renderer){
renderer.innerHTML = JSON.stringify(serverData);
}
},5000); // This will be executed every ~5 seconds.
上面的代碼片段顯示了使用引用節(jié)點(diǎn)或不再需要的數(shù)據(jù)的定時(shí)器的結(jié)果。
該renderer對(duì)象可能會(huì)在某些時(shí)候被替換或刪除糯彬,這會(huì)使interval處理程序封裝的塊變得冗余凭语。如果發(fā)生這種情況,那么處理程序及其依賴項(xiàng)都不會(huì)被收集撩扒,因?yàn)閕nterval需要先停止似扔。這一切都?xì)w結(jié)為存儲(chǔ)和處理負(fù)載數(shù)據(jù)的serverData不會(huì)被收集的原因。
當(dāng)使用監(jiān)視器時(shí)搓谆,你需要確保做了一個(gè)明確的調(diào)用來刪除它們炒辉。
幸運(yùn)的是,大多數(shù)現(xiàn)代瀏覽器都會(huì)為你做這件事:即使你忘記刪除監(jiān)聽器泉手,當(dāng)監(jiān)測(cè)對(duì)象變得無法訪問黔寇,它們就會(huì)自動(dòng)收集監(jiān)測(cè)處理器。這是過去的一些瀏覽器無法處理的情況(例如舊的IE6)斩萌。
看下面的例子:
var element = document.getElementById('launch-button');
var counter = 0;
function onclick(event){
counter++;
element.innerHTML = 'text '+ counter;
}
element.addEventListener('click',onClick);
// Do stuff
element.removeEventListener('click',onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope, both element and onClick will be collected event in old browsers that don't handle cycles well.
由于現(xiàn)代瀏覽器支持垃圾回收機(jī)制缝裤,所以當(dāng)某個(gè)節(jié)點(diǎn)變得不能訪問時(shí),你不再需要調(diào)用removeEventListener,因?yàn)槔厥諜C(jī)制會(huì)恰當(dāng)?shù)奶幚磉@些節(jié)點(diǎn)颊郎。
如果你正在使用jQueryAPI(其他庫和框架也支持這一點(diǎn))憋飞,那么也可以在節(jié)點(diǎn)不用之前刪除監(jiān)聽器。即使應(yīng)用程序在較舊的瀏覽器版本下運(yùn)行姆吭,庫也會(huì)確保沒有內(nèi)存泄露榛做。
3.閉包
JavaScript開發(fā)的是一個(gè)關(guān)鍵方面是閉包。閉包是一個(gè)內(nèi)部函數(shù)猾编,可以訪問外部(封閉函數(shù))函數(shù)的變量瘤睹。由于JavaScript運(yùn)行時(shí)的實(shí)現(xiàn)細(xì)節(jié),可能存在一下形式內(nèi)存泄露:
var theThing = null;
var replaceThing = function(){
var originalThing = theThis;
var unused = function(){
if(originalThing) // 對(duì)‘originalThing’的引用
console.log('hi');
}
theThing = {
longStr: new Array (1000000).join('*'),
someMethod: function(){
console.log("message");
}
};
};
setInterval(replaceThing,1000);
一旦replaceThing被調(diào)用答倡,theThing會(huì)獲取由一個(gè)大數(shù)組和一個(gè)新的閉包(someMthod)組成的新對(duì)象轰传。然而,originalThing會(huì)被unused變量所持有的閉包所引用(這是theThing從以前的調(diào)用變量replaceThing)瘪撇。需要記住的是获茬,一旦在同一父作用域中為閉包創(chuàng)建了閉包的作用域,作用域就被共享了倔既。
在這種情況下恕曲,閉包創(chuàng)建的范圍會(huì)將someMethod共享給unused。然而渤涌,unused有一個(gè)originalThing引用佩谣。即使unused從未使用過,someMethod也可以通過theThing在整個(gè)范圍之外使用replaceThing实蓬。而且someMethod通過unused共享了閉包范圍茸俭,unused必須引用originalThing以便使其它保持活躍(兩封閉之間的整個(gè)共享范圍)吊履。這就阻止了它被收集。
所有這些都可能導(dǎo)致相當(dāng)大的內(nèi)存泄露调鬓。當(dāng)上面的代碼片段一遍又一遍地運(yùn)行時(shí)艇炎,你會(huì)看到內(nèi)存使用率的不斷上升。當(dāng)垃圾收集器運(yùn)行時(shí)腾窝,其內(nèi)存大小不會(huì)縮小缀踪,這種情況會(huì)創(chuàng)建一個(gè)閉包的鏈表,并且每個(gè)閉包范圍都帶有對(duì)大數(shù)組的間接引用虹脯。
4.超出DOM引用
在某些情況下驴娃,開發(fā)人員會(huì)在數(shù)據(jù)結(jié)構(gòu)中存儲(chǔ)DOM節(jié)點(diǎn),例如你想快速更新表格中的幾行內(nèi)的情況归形。如果在字典或數(shù)組中存儲(chǔ)對(duì)每個(gè)DOM行的引用托慨,則會(huì)有兩個(gè)對(duì)同一個(gè)DOM元素的引用:一個(gè)在DOM樹中,另一個(gè)在字典中暇榴。如果你不再需要這些行厚棵,則需要使兩個(gè)引用都無法訪問。
var elements = {
button:document.getElementById('button'),
image:document.getElementById('image'),
};
function doStuff(){
elements.image.src = "http://example.com/image_name.png";
}
function removeImage(){
// The image is a direct child of the body element.
document.body.removeChild(document.getElementById('image'));
// At this point, we still have a reference to #button in the global elements object, In other words, the button element is still in memory and cannot be collected by the GC.
}
在涉及DOM樹內(nèi)的內(nèi)部節(jié)點(diǎn)或葉節(jié)點(diǎn)時(shí)蔼紧,還有一個(gè)額外的因素需要考慮婆硬。如果你在代碼中保留對(duì)表格單元格(標(biāo)簽)的引用,并決定從DOM中刪除該表格奸例,還需要保留對(duì)該特定單元格的引用彬犯,則可能會(huì)出現(xiàn)嚴(yán)重的內(nèi)存泄露,你可能會(huì)認(rèn)為垃圾收集器會(huì)釋放除了那個(gè)單元之外的所有東西查吊,但情況并非如此谐区。由于單元格是表格的一個(gè)子節(jié)點(diǎn),并且子節(jié)點(diǎn)保留著對(duì)父節(jié)點(diǎn)的引用逻卖,所以對(duì)表格單元格的這種引用宋列,會(huì)將整個(gè)表格保存在內(nèi)存中。
總結(jié)
以上內(nèi)容是對(duì)JavaScript常見的四種內(nèi)存泄露分析评也。希望對(duì)JavaScript編程人員有用炼杖。