一篮幢、垃圾回收機(jī)制—GC
JavaScript具有自動(dòng)垃圾回收機(jī)制(GC:Garbage Collecation)大刊,也就是說(shuō),執(zhí)行環(huán)境會(huì)負(fù)責(zé)管理代碼執(zhí)行過(guò)程中使用的內(nèi)存三椿。
原理:垃圾收集器會(huì)定期(周期性)找出那些不在繼續(xù)使用的變量缺菌,然后釋放其內(nèi)存曲尸。
JavaScript垃圾回收的機(jī)制很簡(jiǎn)單:找出不再使用的變量,然后釋放掉其占用的內(nèi)存男翰,但是這個(gè)過(guò)程不是實(shí)時(shí)的另患,因?yàn)槠溟_(kāi)銷(xiāo)比較大,所以垃圾回收器會(huì)按照固定的時(shí)間間隔周期性的執(zhí)行蛾绎。
不再使用的變量也就是生命周期結(jié)束的變量昆箕,當(dāng)然只可能是局部變量,全局變量的生命周期直至瀏覽器卸載頁(yè)面才會(huì)結(jié)束租冠。局部變量只在函數(shù)的執(zhí)行過(guò)程中存在鹏倘,而在這個(gè)過(guò)程中會(huì)為局部變量在棧或堆上分配相應(yīng)的空間顽爹,以存儲(chǔ)它們的值纤泵,然后在函數(shù)中使用這些變量,直至函數(shù)結(jié)束镜粤,而閉包中由于內(nèi)部函數(shù)的原因捏题,外部函數(shù)并不能算是結(jié)束。
還是上代碼說(shuō)明吧:
function fn1() {
var obj = {
name: 'hanzichi',
age: 10
};
}
function fn2() {
var obj = {
name: 'hanzichi',
age: 10
};
return obj;
}
var a = fn1();
var b = fn2();
我們來(lái)看代碼是如何執(zhí)行的肉渴。首先定義了兩個(gè)function公荧,分別叫做fn1和fn2,當(dāng)fn1被調(diào)用時(shí)同规,進(jìn)入fn1的環(huán)境循狰,會(huì)開(kāi)辟一塊內(nèi)存存放對(duì)象{name: 'hanzichi', age: 10},而當(dāng)調(diào)用結(jié)束后券勺,出了fn1的環(huán)境绪钥,那么該塊內(nèi)存會(huì)被js引擎中的垃圾回收器自動(dòng)釋放;在fn2被調(diào)用的過(guò)程中关炼,返回的對(duì)象被全局變量b所指向程腹,所以該塊內(nèi)存并不會(huì)被釋放。
這里問(wèn)題就出現(xiàn)了:到底哪個(gè)變量是沒(méi)有用的盗扒?所以垃圾收集器必須跟蹤到底哪個(gè)變量沒(méi)用跪楞,對(duì)于不再有用的變量打上標(biāo)記缀去,以備將來(lái)收回其內(nèi)存侣灶。用于標(biāo)記的無(wú)用變量的策略可能因?qū)崿F(xiàn)而有所區(qū)別,通常情況下有兩種實(shí)現(xiàn)方式:標(biāo)記清除和引用計(jì)數(shù)缕碎。引用計(jì)數(shù)不太常用褥影,標(biāo)記清除較為常用。
二咏雌、標(biāo)記清除
js中最常用的垃圾回收方式就是標(biāo)記清除凡怎。當(dāng)變量進(jìn)入環(huán)境時(shí)校焦,例如,在函數(shù)中聲明一個(gè)變量统倒,就將這個(gè)變量標(biāo)記為“進(jìn)入環(huán)境”末购。從邏輯上講吨悍,永遠(yuǎn)不能釋放進(jìn)入環(huán)境的變量所占用的內(nèi)存,因?yàn)橹灰獔?zhí)行流進(jìn)入相應(yīng)的環(huán)境,就可能會(huì)用到它們宅静。而當(dāng)變量離開(kāi)環(huán)境時(shí),則將其標(biāo)記為“離開(kāi)環(huán)境”盈匾。
function test(){
var a = 10 ; //被標(biāo)記 忿薇,進(jìn)入環(huán)境
var b = 20 ; //被標(biāo)記 ,進(jìn)入環(huán)境
}
test(); //執(zhí)行完畢 之后 a岳链、b又被標(biāo)離開(kāi)環(huán)境花竞,被回收。
垃圾回收器在運(yùn)行的時(shí)候會(huì)給存儲(chǔ)在內(nèi)存中的所有變量都加上標(biāo)記(當(dāng)然掸哑,可以使用任何標(biāo)記方式)约急。然后,它會(huì)去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標(biāo)記(閉包)苗分。而在此之后再被加上標(biāo)記的變量將被視為準(zhǔn)備刪除的變量烤宙,原因是環(huán)境中的變量已經(jīng)無(wú)法訪(fǎng)問(wèn)到這些變量了。最后俭嘁,垃圾回收器完成內(nèi)存清除工作躺枕,銷(xiāo)毀那些帶標(biāo)記的值并回收它們所占用的內(nèi)存空間。
到目前為止供填,IE拐云、Firefox、Opera近她、Chrome叉瘩、Safari的js實(shí)現(xiàn)使用的都是標(biāo)記清除的垃圾回收策略或類(lèi)似的策略,只不過(guò)垃圾收集的時(shí)間間隔互不相同粘捎。
三薇缅、引用計(jì)數(shù)
引用計(jì)數(shù)的含義是跟蹤記錄每個(gè)值被引用的次數(shù)。當(dāng)聲明了一個(gè)變量并將一個(gè)引用類(lèi)型值賦給該變量時(shí)攒磨,則這個(gè)值的引用次數(shù)就是1泳桦。如果同一個(gè)值又被賦給另一個(gè)變量,則該值的引用次數(shù)加1娩缰。相反灸撰,如果包含對(duì)這個(gè)值引用的變量又取得了另外一個(gè)值,則這個(gè)值的引用次數(shù)減1。當(dāng)這個(gè)值的引用次數(shù)變成0時(shí)浮毯,則說(shuō)明沒(méi)有辦法再訪(fǎng)問(wèn)這個(gè)值了完疫,因而就可以將其占用的內(nèi)存空間回收回來(lái)。這樣债蓝,當(dāng)垃圾回收器下次再運(yùn)行時(shí)壳鹤,它就會(huì)釋放那些引用次數(shù)為0的值所占用的內(nèi)存。
function test(){
var a = {} ; //a的引用次數(shù)為0
var b = a ; //a的引用次數(shù)加1饰迹,為1
var c = a; //a的引用次數(shù)再加1器虾,為2
var b = {}; //a的引用次數(shù)減1,為1
}
Netscape Navigator3是最早使用引用計(jì)數(shù)策略的瀏覽器蹦锋,但很快它就遇到一個(gè)嚴(yán)重的問(wèn)題:循環(huán)引用兆沙。循環(huán)引用指的是對(duì)象A中包含一個(gè)指向?qū)ο驜的指針,而對(duì)象B中也包含一個(gè)指向?qū)ο驛的引用莉掂。
function fn() {
var a = {};
var b = {};
a.pro = b;
b.pro = a;
}
fn();
以上代碼a和b的引用次數(shù)都是2葛圃,fn()執(zhí)行完畢后,兩個(gè)對(duì)象都已經(jīng)離開(kāi)環(huán)境憎妙,在標(biāo)記清除方式下是沒(méi)有問(wèn)題的库正,但是在引用計(jì)數(shù)策略下,因?yàn)閍和b的引用次數(shù)不為0厘唾,所以不會(huì)被垃圾回收器回收內(nèi)存褥符,如果fn函數(shù)被大量調(diào)用,就會(huì)造成內(nèi)存泄露抚垃。在IE7與IE8上喷楣,內(nèi)存直線(xiàn)上升。
我們知道鹤树,IE中有一部分對(duì)象并不是原生js對(duì)象铣焊。例如,其內(nèi)存泄露DOM和BOM中的對(duì)象就是使用C++以COM對(duì)象的形式實(shí)現(xiàn)的罕伯,而COM對(duì)象的垃圾回收機(jī)制采用的就是引用計(jì)數(shù)策略曲伊。因此,即使IE的js引擎采用標(biāo)記清除策略來(lái)實(shí)現(xiàn)追他,但js訪(fǎng)問(wèn)的COM對(duì)象依然是基于引用計(jì)數(shù)策略的坟募。換句話(huà)說(shuō),只要在IE中涉及COM對(duì)象邑狸,就會(huì)存在循環(huán)引用的問(wèn)題懈糯。
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.e = element;
element.o = myObject;
這個(gè)例子在一個(gè)DOM元素(element)與一個(gè)原生js對(duì)象(myObject)之間創(chuàng)建了循環(huán)引用。其中推溃,變量myObject有一個(gè)名為element的屬性指向element對(duì)象昂利;而變量element也有一個(gè)屬性名為o回指myObject届腐。由于存在這個(gè)循環(huán)引用铁坎,即使例子中的DOM從頁(yè)面中移除蜂奸,它也永遠(yuǎn)不會(huì)被回收。
看上面的例子硬萍,有同學(xué)回覺(jué)得太弱了扩所,誰(shuí)會(huì)做這樣無(wú)聊的事情,其實(shí)我們是不是就在做
window.onload = function outerFunction(){
var obj = document.getElementById("element");
obj.onclick = function innerFunction(){};
};
這段代碼看起來(lái)沒(méi)什么問(wèn)題朴乖,但是obj引用了document.getElementById(“element”)祖屏,而document.getElementById(“element”)的onclick方法會(huì)引用外部環(huán)境中德變量,自然也包括obj买羞,是不是很隱蔽啊袁勺。
解決辦法
最簡(jiǎn)單的方式就是自己手工解除循環(huán)引用,比如剛才的函數(shù)可以這樣
myObject.element = null;
element.o = null;
window.onload = function outerFunction(){
var obj = document.getElementById("element");
obj.onclick = function innerFunction(){};
obj = null;
};
將變量設(shè)置為null意味著切斷變量與它此前引用的值之間的連接畜普。當(dāng)垃圾回收器下次運(yùn)行時(shí)期丰,就會(huì)刪除這些值并回收它們占用的內(nèi)存。
要注意的是吃挑,IE9+并不存在循環(huán)引用導(dǎo)致Dom內(nèi)存泄露問(wèn)題钝荡,可能是微軟做了優(yōu)化,或者Dom的回收方式已經(jīng)改變
四舶衬、內(nèi)存管理
1埠通、什么時(shí)候觸發(fā)垃圾回收?
垃圾回收器周期性運(yùn)行逛犹,如果分配的內(nèi)存非常多端辱,那么回收工作也會(huì)很艱巨,確定垃圾回收時(shí)間間隔就變成了一個(gè)值得思考的問(wèn)題虽画。IE6的垃圾回收是根據(jù)內(nèi)存分配量運(yùn)行的掠手,當(dāng)環(huán)境中存在256個(gè)變量、4096個(gè)對(duì)象狸捕、64k的字符串任意一種情況的時(shí)候就會(huì)觸發(fā)垃圾回收器工作喷鸽,看起來(lái)很科學(xué),不用按一段時(shí)間就調(diào)用一次灸拍,有時(shí)候會(huì)沒(méi)必要做祝,這樣按需調(diào)用不是很好嗎?但是如果環(huán)境中就是有這么多變量等一直存在鸡岗,現(xiàn)在腳本如此復(fù)雜混槐,很正常,那么結(jié)果就是垃圾回收器一直在工作轩性,這樣瀏覽器就沒(méi)法兒玩兒了声登。
微軟在IE7中做了調(diào)整,觸發(fā)條件不再是固定的,而是動(dòng)態(tài)修改的悯嗓,初始值和IE6相同件舵,如果垃圾回收器回收的內(nèi)存分配量低于程序占用內(nèi)存的15%,說(shuō)明大部分內(nèi)存不可被回收脯厨,設(shè)的垃圾回收觸發(fā)條件過(guò)于敏感铅祸,這時(shí)候把臨街條件翻倍,如果回收的內(nèi)存高于85%合武,說(shuō)明大部分內(nèi)存早就該清理了临梗,這時(shí)候把觸發(fā)條件置回。這樣就使垃圾回收工作職能了很多
2稼跳、合理的GC方案
1)盟庞、Javascript引擎基礎(chǔ)GC方案是(simple GC):mark and sweep(標(biāo)記清除),即:
(1)遍歷所有可訪(fǎng)問(wèn)的對(duì)象汤善。
(2)回收已不可訪(fǎng)問(wèn)的對(duì)象什猖。
2)、GC的缺陷
和其他語(yǔ)言一樣萎津,javascript的GC策略也無(wú)法避免一個(gè)問(wèn)題:GC時(shí)卸伞,停止響應(yīng)其他操作,這是為了安全考慮锉屈。而Javascript的GC在100ms甚至以上荤傲,對(duì)一般的應(yīng)用還好,但對(duì)于JS游戲颈渊,動(dòng)畫(huà)對(duì)連貫性要求比較高的應(yīng)用遂黍,就麻煩了。這就是新引擎需要優(yōu)化的點(diǎn):避免GC造成的長(zhǎng)時(shí)間停止響應(yīng)俊嗽。
3)雾家、GC優(yōu)化策略
David大叔主要介紹了2個(gè)優(yōu)化方案,而這也是最主要的2個(gè)優(yōu)化方案了:
(1)分代回收(Generation GC)這個(gè)和Java回收策略思想是一致的绍豁。目的是通過(guò)區(qū)分“臨時(shí)”與“持久”對(duì)象芯咧;多回收“臨時(shí)對(duì)象”區(qū)(young generation),少回收“持久對(duì)象”區(qū)(tenured generation)竹揍,減少每次需遍歷的對(duì)象敬飒,從而減少每次GC的耗時(shí)。如圖:
這里需要補(bǔ)充的是:對(duì)于tenured generation對(duì)象芬位,有額外的開(kāi)銷(xiāo):把它從young generation遷移到tenured generation无拗,另外,如果被引用了昧碉,那引用的指向也需要修改英染。
(2)增量GC這個(gè)方案的思想很簡(jiǎn)單揽惹,就是“每次處理一點(diǎn),下次再處理一點(diǎn)四康,如此類(lèi)推”搪搏。如圖:
這種方案,雖然耗時(shí)短箭养,但中斷較多慕嚷,帶來(lái)了上下文切換頻繁的問(wèn)題哥牍。
因?yàn)槊糠N方案都其適用場(chǎng)景和缺點(diǎn)毕泌,因此在實(shí)際應(yīng)用中,會(huì)根據(jù)實(shí)際情況選擇方案嗅辣。
比如:低 (對(duì)象/s) 比率時(shí)撼泛,中斷執(zhí)行GC的頻率,simple GC更低些澡谭;如果大量對(duì)象都是長(zhǎng)期“存活”愿题,則分代處理優(yōu)勢(shì)也不大。