什么是垃圾回收機(jī)制?
和java,c#一樣,javascript也有垃圾回收的機(jī)制,比如說(shuō)c++和c就沒(méi)有垃圾回收機(jī)制。可能有這么一種傾向,垃圾回收機(jī)制必須有一種平臺(tái)來(lái)進(jìn)行回收。比如說(shuō)下面將講的javascript的執(zhí)行環(huán)境V8就會(huì)負(fù)責(zé)管理代碼執(zhí)行過(guò)程中的垃圾回收。
javascript具有自動(dòng)垃圾回收機(jī)制,執(zhí)行環(huán)境會(huì)負(fù)責(zé)管理代碼執(zhí)行過(guò)程中使用的內(nèi)存。原理就是找出那些不再繼續(xù)使用的變量,然后釋放其占有內(nèi)存。這整個(gè)過(guò)程也會(huì)按照一個(gè)固定的事件周期性的整形(時(shí)時(shí)的話(huà)開(kāi)銷(xiāo)太大)。
垃圾回收機(jī)制回收的是什么?
剛剛原理中提到要找出不再使用的變量,什么是不再使用的對(duì)象呢?不再使用的變量也就是生命周期結(jié)束的變量。目前javascript有兩種變量,全局變量和在函數(shù)中產(chǎn)生的局部變量(暫不考慮ES6中的塊級(jí)作用域)。
全局變量的聲明周期一直持續(xù)到瀏覽器關(guān)閉頁(yè)面才會(huì)清除,而局部變量只是在函數(shù)執(zhí)行時(shí)存在,而在這個(gè)過(guò)程中會(huì)為局部變量在棧或者堆上分配相應(yīng)的空間,來(lái)存儲(chǔ)他們的值,然后當(dāng)函數(shù)要使用這些變量的值時(shí)再取出來(lái)使用。一直到函數(shù)結(jié)束(閉包會(huì)不同)。
一旦函數(shù)結(jié)束,局部變量就不需要了,這時(shí)候就可以釋放他們的內(nèi)存。
var global = "I'm global";
function test(){
var local = “I m local” ;
}
test();
這個(gè)例子里面,global在關(guān)閉瀏覽器時(shí)釋放,local在函數(shù)test結(jié)束后,釋放。
什么時(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)用一次,但是如果環(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ā)條件置回。這樣就使垃圾回收工作智能了很多。
js的兩種回收機(jī)制
標(biāo)記清除(mark and sweep)
從語(yǔ)義上理解就比較好理解了,就是當(dāng)變量進(jìn)入到某個(gè)環(huán)境中的時(shí)候就把這個(gè)變量標(biāo)記一下,比如標(biāo)記為“進(jìn)入環(huán)境”,當(dāng)離開(kāi)的時(shí)候就把這個(gè)變量的標(biāo)記給清除掉,比如是“離開(kāi)環(huán)境”,而在這后面還有標(biāo)記的變量將被視為準(zhǔn)備刪除的變量。
垃圾收集器在運(yùn)行的時(shí)候會(huì)給存儲(chǔ)在內(nèi)存中的所有變量都加上標(biāo)記(可以使用任何標(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)存空間。
這是javascript最常見(jiàn)的垃圾回收方式。至于上面有說(shuō)到的標(biāo)記,到底該如何標(biāo)記?好像是有很多方法,比如特殊位翻轉(zhuǎn),維護(hù)一個(gè)列表什么的。
工作原理:是當(dāng)變量進(jìn)入環(huán)境時(shí),將這個(gè)變量標(biāo)記為“進(jìn)入環(huán)境”。當(dāng)變量離開(kāi)環(huán)境時(shí),則將其標(biāo)記為“離開(kāi)環(huán)境”。標(biāo)記“離開(kāi)環(huán)境”的就回收內(nèi)存。
工作流程:
- 垃圾回收器,在運(yùn)行的時(shí)候會(huì)給存儲(chǔ)在內(nèi)存中的所有變量都加上標(biāo)記。
- 去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標(biāo)記。
- 再被加上標(biāo)記的會(huì)被視為準(zhǔn)備刪除的變量。
- 垃圾回收器完成內(nèi)存清除工作,銷(xiāo)毀那些帶標(biāo)記的值并回收他們所占用的內(nèi)存空間。
引用計(jì)數(shù)(reference counting)
引用計(jì)數(shù)的含義是跟蹤記錄每個(gè)值被引用的次數(shù),當(dāng)聲明一個(gè)變量并將一個(gè)引用類(lèi)型的值賦給該變量時(shí),這個(gè)時(shí)候的引用類(lèi)型的值就會(huì)是引用次數(shù)+1了。如果同一個(gè)值又被賦給另外一個(gè)變量,則該值的引用次數(shù)又+1。
相反如果包含這個(gè)值的引用的變量又取得另外一個(gè)值,即被重新賦了值,那么這個(gè)值的引用就減一。當(dāng)這個(gè)值的引用次數(shù)變成0時(shí),表示沒(méi)有用到這個(gè)值,這個(gè)值也無(wú)法訪(fǎng)問(wèn),因此環(huán)境就會(huì)回收這個(gè)值所占用的內(nèi)存空間。這樣,當(dāng)垃圾收集器下次再運(yùn)行時(shí),它就會(huì)釋放引用次數(shù)為0的值所占用的內(nèi)存。
但是剛剛也說(shuō)了,第一種標(biāo)記清除是最經(jīng)常用到的,那么這個(gè)看起來(lái)這么好的引用計(jì)數(shù)為啥不被別人用了呢?
工作原理:跟蹤記錄每個(gè)值被引用的次數(shù)。
工作流程:
- 聲明了一個(gè)變量并將一個(gè)引用類(lèi)型的值賦值給這個(gè)變量,這個(gè)引用類(lèi)型值的引用次數(shù)就是1。
- 同一個(gè)值又被賦值給另一個(gè)變量,這個(gè)引用類(lèi)型值的引用次數(shù)加1.
- 當(dāng)包含這個(gè)引用類(lèi)型值的變量又被賦值成另一個(gè)值了,那么這個(gè)引用類(lèi)型值的引用次數(shù)減1.
- 當(dāng)引用次數(shù)變成0時(shí),說(shuō)明沒(méi)辦法訪(fǎng)問(wèn)這個(gè)值了。
- 當(dāng)垃圾收集器下一次運(yùn)行時(shí),它就會(huì)釋放引用次數(shù)是0的值所占的內(nèi)存。
循環(huán)引用時(shí)的問(wèn)題
但是循環(huán)引用的時(shí)候就會(huì)釋放不掉內(nèi)存。循環(huán)引用就是對(duì)象A中包含另一個(gè)指向?qū)ο驜的指針,B中也包含一個(gè)指向A的引用。因?yàn)镮E中的BOM、DOM的實(shí)現(xiàn)使用了COM,而COM對(duì)象使用的垃圾收集機(jī)制是引用計(jì)數(shù)策略。所以會(huì)存在循環(huán)引用的問(wèn)題。
解決:手工斷開(kāi)js對(duì)象和DOM之間的鏈接。賦值為null。IE9把DOM和BOM轉(zhuǎn)換成真正的JS對(duì)象了,所以避免了這個(gè)問(wèn)題因?yàn)檫@個(gè)過(guò)程中會(huì)出現(xiàn)一個(gè)循環(huán)引用的問(wèn)題!簡(jiǎn)單點(diǎn)來(lái)說(shuō)就是一個(gè)對(duì)象小a的屬性,引用了對(duì)象小b。小b對(duì)象也有一個(gè)屬性引用了小a,那么小a,小b互相引用對(duì)方,也就造成了循環(huán)引用的問(wèn)題啦。舉個(gè)栗子:
function test(){
var a = {};
var b = {};
a.property = b;
b.property = a;
}
這就是一個(gè)很明顯的循環(huán)引用了,小a和小b通過(guò)各自的屬性互相引用,導(dǎo)致了內(nèi)存無(wú)法釋放。(有那么一點(diǎn)點(diǎn)的感覺(jué)像死鎖。。。。)即使是再test()執(zhí)行完后,如果使用標(biāo)記清除是沒(méi)有問(wèn)題的,離開(kāi)環(huán)境的時(shí)候就會(huì)被清除。但是引用計(jì)數(shù)不行,因?yàn)檫@兩個(gè)對(duì)象的引用次數(shù)還是存在,不會(huì)變成0,所以其占用空間也不會(huì)清理,如果這個(gè)函數(shù)被調(diào)用多次,就會(huì)不斷有內(nèi)存被占用。造成了內(nèi)存泄露。
IE中有一部分對(duì)象并不是原生JavaScript對(duì)象。例如,其BOM和DOM中的對(duì)象就是使用C++以COM(Component Object Model)對(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ì)象如BOM,DOM還是基于引用計(jì)數(shù)的策略的,也就是說(shuō)只要在IE中涉及到COM對(duì)象,也就會(huì)存在循環(huán)引用的問(wèn)題。比如說(shuō)第一種情況:一個(gè)DOM元素和一個(gè)原生的js對(duì)象之間的循環(huán)引用
var ele = document.getElementById("ele");
var obj = {};
obj.property = ele;
ele.property = obj; //這種情況應(yīng)該手動(dòng)設(shè)置,在不適用的時(shí)候手工斷開(kāi)js和dom元素之間的連接
obj.property = null;
ele.property = null;
比如第二種情況是閉包的作用域鏈中包含著一個(gè)html元素,那么這個(gè)元素?zé)o法被銷(xiāo)毀
window.onload = function outerFunction(){
var ele= document.getElementById("element");
ele.onclick = function (){
console.log(ele.id);
}
}
上面這個(gè)代碼創(chuàng)建了一個(gè)作為ele元素處理程序的閉包,而這個(gè)閉包則又創(chuàng)建了一個(gè)循環(huán)引用。匿名函數(shù)中保存了一個(gè)outerFunction()的活動(dòng)對(duì)象的引用,因此就會(huì)導(dǎo)致無(wú)法減少ele的引用。可以改成下面這個(gè):
window.onload = function outerFunction(){
var ele= document.getElementById("element");
var id = ele.id;
ele.onclick = function (){
console.log(id);
}
ele = null;
}
在上面的代碼中,通過(guò)把ele.id 的一個(gè)副本保存在一個(gè)變量中,并且在閉包中引用改變量消除了循環(huán)引用。
必須要記住:閉包會(huì)引用包含函數(shù)的整個(gè)活動(dòng)對(duì)象,而其中包含著elem。即使閉包不直接引用ele(比如上面的例子我們不用id),包含函數(shù)的多動(dòng)對(duì)象中也依舊會(huì)保存一個(gè)引用。因此,有必要把ele變量設(shè)置為null。這樣就能夠解除對(duì)DOM對(duì)象的引用,順利地減少其引用數(shù),確保正常回收其占用的內(nèi)存。
將變量設(shè)置為null意味著切斷變量與它此前引用的值之間的連接。當(dāng)垃圾收集器下次運(yùn)行時(shí),就會(huì)刪除這些值并回收它們占用的內(nèi)存。
垃圾回收機(jī)制的好處和壞處
好處:大幅簡(jiǎn)化程序的內(nèi)存管理代碼,減輕程序猿負(fù)擔(dān),并且減少因?yàn)殚L(zhǎng)時(shí)間運(yùn)轉(zhuǎn)而帶來(lái)的內(nèi)存泄露問(wèn)題。
壞處:自動(dòng)回收意味著程序猿無(wú)法掌控內(nèi)存。ECMAScript中沒(méi)有暴露垃圾回收的借口,我們無(wú)法強(qiáng)迫其進(jìn)行垃圾回收,更加無(wú)法干預(yù)內(nèi)存管理。