前言:這篇文章的主要內(nèi)容由翻譯而來佑菩,原文鏈接。但是大體內(nèi)容與原文不盡相同忍疾,刪除了一些內(nèi)容闯传,同時新增部分內(nèi)容。由于本文大部分內(nèi)容是翻譯而來膝昆,若有理解不當(dāng)之處還望諒解并指出丸边,我會盡快進(jìn)行修改。(內(nèi)心:如果有什么不對的地方還希望大家指出荚孵,反正我也不會改 妹窖。玩笑話玩笑話 別當(dāng)真!)
概述
在一些語言中收叶,開發(fā)人員需要手動的使用原生語句來顯示的分配和釋放內(nèi)存骄呼。但是在許多高級語言中,這些過程都會被自動的執(zhí)行。在JavaScript中蜓萄,變量(對象隅茎,字符串,等等)創(chuàng)建的時候為其分配內(nèi)存嫉沽,當(dāng)不再被使用的時候會“自動地”釋放這些內(nèi)存辟犀,這個過程被稱為垃圾回收。這個看似“自動的”釋放資源的本質(zhì)是一個混亂的來源绸硕,給JavaScript(和其他高等級語言)開發(fā)者可以不去關(guān)心內(nèi)存管理的錯誤印象堂竟。這是一個很大的錯誤。
內(nèi)存泄漏
內(nèi)存泄漏(Memory Leak)是指程序中己動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放玻佩,造成系統(tǒng)內(nèi)存的浪費出嘹,導(dǎo)致程序運行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。
內(nèi)存泄漏缺陷具有隱蔽性咬崔、積累性的特征税稼,比其他內(nèi)存非法訪問錯誤更難檢測。因為內(nèi)存泄漏的產(chǎn)生原因是內(nèi)存塊未被釋放垮斯,屬于遺漏型缺陷而不是過錯型缺陷郎仆。此外,內(nèi)存泄漏通常不會直接產(chǎn)生可觀察的錯誤癥狀兜蠕,而是逐漸積累丸升,降低系統(tǒng)整體性能,極端的情況下可能使系統(tǒng)崩潰牺氨。
內(nèi)存生命周期
無論使用哪一種編程語言,內(nèi)存的生命周期幾乎總是一模一樣的
分配內(nèi)存、使用內(nèi)存墩剖、釋放內(nèi)存猴凹。
在這里我們主要討論內(nèi)存的回收。
引用計數(shù)垃圾回收
這是最簡單的垃圾回收算法岭皂。一個對象在沒有被其他的引用指向的時候就被認(rèn)為“可回收的”郊霎。
對JS引用類型不熟悉的請先百度引用類型,理解了值類型(基本類型)和引用類型之后才能理解下面的代碼
var obj1 = {
obj2: {
x: 1
}
};
//2個對象被創(chuàng)建爷绘。 obj2被obj1引用书劝,并且作為obj1的屬性存在。這里并沒有可以被回收的土至。
//obj1和obj2都指向了{(lán)obj2: {x: 1}}這個對象购对,這個示例中用`原來的對象`來表示這個對象。
var obj3 = obj1; //obj3也引用了obj1指向的對象陶因。
obj1 = 1; // obj1不引用原來的對象了骡苞。此時原來的對象只有obj3在引用。
var obj4 = obj3.obj2; //obj4引用了obj3對象的obj2屬性,
//此時obj2對象有2個引用解幽,一個是作為obj3的一個屬性贴见,一個是作為obj4變量。
obj3 = 1;
// 咦躲株,obj1原來對象只有obj3在引用片部,現(xiàn)在obj3也沒用在引用了。
// obj1原來的對象就淪為了一只單身狗霜定,于是乎抓狗大隊就來帶走了它档悠。(好吧、其實內(nèi)存就可以被回收了)然爆。
// 然而 obj2對象依然有人愛(被obj4引用)站粟。所以obj2的內(nèi)存就不會被垃圾回收。
obj4 = null;
// obj2內(nèi)心在吶喊:小姐姐不要離開我 QOQ≡瘢現(xiàn)在obj2也沒有被引用了奴烙,引用計數(shù)就是0
也就是可以被回收了。
簡而言之~剖张,如果內(nèi)存有人愛切诀,那就不會被回收服爷。如果是單身狗的話矾端,[手動滑稽]。
循環(huán)引用會造成麻煩
引用計數(shù)在涉及循環(huán)引用的時候有一個缺陷盒刚。在下面的例子中顾犹,創(chuàng)建了2個對象倒庵,并且相互引用,這樣創(chuàng)建了一個循環(huán)炫刷。因此他們實際上是無用的擎宝,可以被釋放。然而引用計數(shù)算法考慮到2個對象中的每一個至少被引用了一次浑玛,因此都不可以被回收绍申。
function f() {
var o1 = {};
var o2 = {};
o1.p = o2;
o2.p = o1;
}
f();
單身狗心里千萬頭草泥馬在奔騰(我特么也會自己牽自己手啊,我也會假裝情侶拍照肮苏谩)標(biāo)記清除算法
別以為你假裝不是單身狗就拿你沒辦法了极阅,這個算法確定了對象是否可以被達(dá)到。
這個算法包含了以下步驟:
- 從‘根’上生成一個列表(通常是以全局變量為根)涨享。在JS中
window
對象可以作為一個'根' - 所有的'根'都被標(biāo)記為活躍的筋搏,所有的子變量也被遞歸檢查。能夠從'根'上到達(dá)的都不會被認(rèn)為成垃圾厕隧。
- 沒有被標(biāo)記為活躍的就被認(rèn)為成垃圾拆又。這些內(nèi)存就會被釋放儒旬。
上圖就是標(biāo)記清除的動作。
在之前的例子中帖族,雖然兩個變量相互引用栈源,但在函數(shù)執(zhí)行完之后,這個兩個變量都沒有被window
對象上的任何對象所引用竖般。因此甚垦,他們會被認(rèn)為不可到達(dá)的。
4種常見的JS內(nèi)存泄漏
1:全局變量
JavaScript用一個有趣的方式管理未被聲明的變量:對未聲明的變量的引用在全局對象里創(chuàng)建一個新的變量涣雕。在瀏覽器的情況下艰亮,這個全局對象是window
。換句話說:
function foo(arg) {
bar = 'some text';
}
//等同于
function foo(arg) {
window.bar = 'some text';
}
如果bar
被假定只在foo
函數(shù)的作用域里引用挣郭,但是你忘記了使用var
去聲明它迄埃,一個意外的全局變量就被聲明了。
在這個例子里兑障,泄漏一個簡單的字符并不會造成很大的傷害侄非,但是它確實有可能變得更糟。
有時有會通過this
來創(chuàng)建意外的全局變量流译。
為了防止這些問題發(fā)生逞怨,可以在你的JaveScript文件開頭使用
'use strict'
;福澡。這個可以使用一種嚴(yán)格的模式解析JavaScript來阻止意外的全局變量叠赦。
如果有時全局變量被用于暫時儲存大量的數(shù)據(jù)或者涉及到的信息,那么在使用完之后應(yīng)該指定為null或者重新分配革砸。
2:被遺忘的定時器或者回調(diào)
還是來個栗子吧除秀,定時器可能會產(chǎn)生對不再需要的DOM節(jié)點或者數(shù)據(jù)的引用。
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每五秒會執(zhí)行一次
renderer
對象在將來有可能被移除算利,讓interval
沒有存在的意義鳞仙。然而當(dāng)處理器interval
仍然起作用時,renderer
并不能被回收(interval
在對象被移除時需要被停止),如果interval
不能被回收笔时,它的依賴也不可能被回收。這就意味著serverData
仗岸,大概保存了大量的數(shù)據(jù)允耿,也不可能被回收。
如今扒怖,大部分的瀏覽器都能而且會在對象變得不可到達(dá)的時候回收觀察處理器较锡,甚至監(jiān)聽器沒有被明確的移除掉。在對象被處理之前盗痒,最好也要顯式地刪除這些觀察者蚂蕴。
var element = document.getElementById('launch-button');
var counter = 0;
function onClick(event) {
counter++;
element.innerHtml = 'text ' + counter;
}
element.addEventListener('click', onClick);
// 做一些其他的事情
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
如今低散,現(xiàn)在的瀏覽器(包括IE和Edge)使用現(xiàn)代的垃圾回收算法,可以立即發(fā)現(xiàn)并處理這些循環(huán)引用骡楼。換句話說熔号,在一個節(jié)點刪除之前也不是必須要調(diào)用removeEventListener。
框架和插件例如jQuqery在處理節(jié)點(當(dāng)使用具體的api的時候)之前會移除監(jiān)聽器鸟整。這個是插件內(nèi)部的處理可以確保不會產(chǎn)生內(nèi)存泄漏引镊,甚至運行在有問題的瀏覽器上(哈哈哈 說的就是IE6)。
3: 閉包
閉包是javascript開發(fā)的一個關(guān)鍵方面篮条,一個內(nèi)部函數(shù)使用了外部(封閉)函數(shù)的變量弟头。由于JavaScript運行的細(xì)節(jié),它可能以下面的方式造成內(nèi)存泄漏:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) console.log('hi') //引用了originalThing
};
theThing = {
longStr: new Array(1000000).jojin('*'),
someMethod: function (){
console.log('message');
}
};
};
setInterval(replaceThing,1000);
這些代碼做了一件事情涉茧,每次relaceThing
被調(diào)用赴恨,theThing
獲得一個包含大量數(shù)據(jù)和新的閉包(someMethod
)的對象。同時伴栓,變量unused
引用了originalThing
(theThing
是上一次函數(shù)被調(diào)用時產(chǎn)生的)伦连。已經(jīng)有點困惑了吧?最重要的事情是一旦為同一父域中的作用域產(chǎn)生閉包挣饥,則該作用域是共享的除师。
在這個案例中,someMethod
和unused
共享閉包作用域扔枫,unused
引用了originalThing
,這阻止了originalThing
的回收汛聚,盡管unused
不會被使用,但是someMethod
依然可以通過theThing
來訪問replaceThing
作用域外的變量(例如某些全局的)。
4:來自DOM的引用
在你要重復(fù)的操作DOM節(jié)點的時候短荐,存儲DOM節(jié)點是十分有用的倚舀。但是在你需要移除DOM節(jié)點的時候,需要確保移除DOM tree和代碼中儲存的引用忍宋。
var element = {
image: document.getElementById('image'),
button: document.getElementById('button')
};
//Do some stuff
document.body.removeChild(document.getElementById('image'));
//這個時候 雖然從dom tree中移除了id為image的節(jié)點痕貌,但是還保留了一個對該節(jié)點的引用。于是image仍然不能被回收糠排。
當(dāng)涉及到DOM樹內(nèi)部或子節(jié)點時舵稠,需要考慮額外的考慮因素。例如入宦,你在JavaScript中保持對某個表格的特定單元格的引用哺徊。有一天你決定從DOM中移除表格但是保留了對單元格的引用。你也許會認(rèn)為除了單元格其他的都會被回收乾闰。實際并不是這樣的:單元格是表格的一個子節(jié)點落追,子節(jié)點保持了對父節(jié)點的引用。確切的說涯肩,JS代碼中對單元格的引用造成了整個表格被留在內(nèi)存中了轿钠,所以在移除有被引用的節(jié)點時候要移除其子節(jié)點巢钓。
總結(jié)
- 小心使用全局變量,盡量不要使用全局變量來存儲大量數(shù)據(jù)疗垛,如果是暫時使用症汹,要在使用完成之后手動指定為null或者重新分配
- 如果使用了定時器,在無用的時候要記得清除继谚。如果為DOM節(jié)點綁定了事件監(jiān)聽器烈菌,在移除節(jié)點時要先注銷事件監(jiān)聽器。
- 小心閉包的使用花履。如果掌握不好芽世,至少在使用大量數(shù)據(jù)的時候仔細(xì)考量。在使用遞歸的時候也要非常小心(例如用canvas做小游戲)诡壁。
- 在移除DOM節(jié)點的時候要確保在代碼中沒有對節(jié)點的引用济瓢,這樣才能完全的移除節(jié)點。在移除父節(jié)點之前要先移除子節(jié)點妹卿。