今天看了一下關(guān)于垃圾回收的知識稽煤,來總結(jié)一下~
我們知道,JavaScript之所以能在瀏覽器環(huán)境和NodeJS環(huán)境運(yùn)行囚戚,都是因?yàn)橛蠽8引擎在幕后保駕護(hù)航酵熙。從編譯、內(nèi)存分配驰坊、運(yùn)行以及垃圾回收等整個(gè)過程匾二,都離不開它。
為什么要有垃圾回收
在C語言和C++語言中,我們?nèi)绻胍_辟一塊堆內(nèi)存的話察藐,需要先計(jì)算需要內(nèi)存的大小皮璧,然后自己通過malloc函數(shù)去手動分配,在用完之后分飞,還要時(shí)刻記得用free函數(shù)去清理釋放悴务,否則這塊內(nèi)存就會被永久占用,造成內(nèi)存泄露譬猫。
但是我們在寫JavaScript的時(shí)候惨寿,卻沒有這個(gè)過程,因?yàn)槿思乙呀?jīng)替我們封裝好了删窒,V8引擎會根據(jù)你當(dāng)前定義對象的大小去自動申請分配內(nèi)存裂垦。
不需要我們?nèi)ナ謩庸芾韮?nèi)存了,所以自然要有垃圾回收肌索,否則的話只分配不回收蕉拢,豈不是沒多長時(shí)間內(nèi)存就被占滿了嗎,導(dǎo)致應(yīng)用崩潰诚亚。
垃圾回收的好處是不需要我們?nèi)ス芾韮?nèi)存晕换,把更多的精力放在實(shí)現(xiàn)復(fù)雜應(yīng)用上,但壞處也來自于此站宗,不用管理了闸准,就有可能在寫代碼的時(shí)候不注意,造成循環(huán)引用等情況梢灭,導(dǎo)致內(nèi)存泄露夷家。
內(nèi)存泄漏:不再用到的內(nèi)存,沒有及時(shí)釋放敏释,內(nèi)存被占用库快,這就叫做內(nèi)存泄漏
1. 什么是垃圾?
即沒有被引用的對象都是垃圾
- 所有全局變量都不是垃圾
- 如果是存在函數(shù)內(nèi)的局部變量钥顽,當(dāng)這個(gè)函數(shù)執(zhí)行完了义屏,這個(gè)變量就消除了,但是在執(zhí)行過程中不能被消除蜂大,因?yàn)檫@個(gè)函數(shù)引用著它闽铐,全局變量又引用著這個(gè)函數(shù)
- 如果存在雙引用,把其中一個(gè)引用給置為null奶浦,還有另一個(gè)引用兄墅,也不會成為垃圾
- 如果存在環(huán)引用,如果引用你的對象全松開了手财喳,你就成為了垃圾
環(huán)引用與外界至少要有一個(gè)橋梁察迟,外界有人引用著環(huán)斩狱,不然整個(gè)環(huán)就成了垃圾
2. 如何判斷是否可以回收
1) 標(biāo)記清除
當(dāng)變量進(jìn)入環(huán)境(例如,在函數(shù)中聲明一個(gè)變量)時(shí)扎瓶,就將這個(gè)變量標(biāo)記為“進(jìn)入環(huán)境”所踊。從邏輯上講,永遠(yuǎn)不能釋放進(jìn)入環(huán)境的變量所占用的內(nèi)存概荷,因?yàn)橹灰獔?zhí)行流進(jìn)入相應(yīng)的環(huán)境秕岛,就可能會用到它們。而當(dāng)變量離開環(huán)境時(shí)误证,則將其標(biāo)記為“離開環(huán)境”继薛。
可以使用任何方式來標(biāo)記變量。比如愈捅,可以通過翻轉(zhuǎn)某個(gè)特殊的位來記錄一個(gè)變量何時(shí)進(jìn)入環(huán)境遏考,或者使用一個(gè)“進(jìn)入環(huán)境的”變量列表及一個(gè)“離開環(huán)境的”變量列表來跟蹤哪個(gè)變量發(fā)生了變化。如何標(biāo)記變量并不重要蓝谨,關(guān)鍵在于采取什么策略灌具。
(1)垃圾收集器在運(yùn)行的時(shí)候會給存儲在內(nèi)存中的所有變量都加上標(biāo)記(當(dāng)然,可以使用任何標(biāo)記方式)譬巫。
(2)然后咖楣,它會去掉運(yùn)行環(huán)境中的變量以及被環(huán)境中變量所引用的變量的標(biāo)記
(3)此后,依然有標(biāo)記的變量就被視為準(zhǔn)備刪除的變量芦昔,原因是在運(yùn)行環(huán)境中已經(jīng)無法訪問到這些變量了诱贿。
(4)最后,垃圾收集器完成內(nèi)存清除工作咕缎,銷毀那些帶標(biāo)記的值并回收它們所占用的內(nèi)存空間珠十。
目前,IE锨阿、Firefox宵睦、Opera、Chrome和Safari的JavaScript實(shí)現(xiàn)使用的都是標(biāo)記清除式的垃圾回收策略(或類似的策略)墅诡,只不過垃圾收集的時(shí)間間隔互有不同。
當(dāng)一個(gè)對象和其關(guān)聯(lián)對象不再通過引用關(guān)系被當(dāng)前root引用了桐智,這個(gè)對象就會被垃圾回收末早。
2)引用計(jì)數(shù)
引用計(jì)數(shù)的垃圾收集策略不太常見。含義是跟蹤記錄每個(gè)值被引用的次數(shù)说庭。當(dāng)聲明了一個(gè)變量并將一個(gè)引用類型值賦給該變量時(shí)然磷,則這個(gè)值的引用次數(shù)就是1。
如果同一個(gè)值又被賦給另一個(gè)變量刊驴,則該值的引用次數(shù)加1姿搜。相反寡润,如果包含對這個(gè)值引用的變量改變了引用對象,則該值引用次數(shù)減1舅柜。
當(dāng)這個(gè)值的引用次數(shù)變成0時(shí)梭纹,則說明沒有辦法再訪問這個(gè)值了,因而就可以將其占用的內(nèi)存空間回收回來致份。
這樣变抽,當(dāng)垃圾收集器下次再運(yùn)行時(shí),它就會釋放那些引用次數(shù)為0的值所占用的內(nèi)存氮块。
Netscape Navigator 3.0是最早使用引用計(jì)數(shù)策略的瀏覽器绍载,但很快它就遇到了一個(gè)嚴(yán)重的問題:循環(huán)引用。
循環(huán)引用是指對象A中包含一個(gè)指向?qū)ο驜的指針滔蝉,而對象B中也包含一個(gè)指向?qū)ο驛的引用击儡,看個(gè)例子:
function foo () {
var objA = new Object();
var objB = new Object();
objA.otherObj = objB;
objB.anotherObj = objA;
}
這個(gè)例子中,objA和objB通過各自的屬性相互引用蝠引,也就是說曙痘,這兩個(gè)對象的引用次數(shù)都是2。
在采用標(biāo)記清除策略的實(shí)現(xiàn)中立肘,由于函數(shù)執(zhí)行后边坤,這兩個(gè)對象都離開了作用域,因此這種相互引用不是問題谅年。
但在采用引用次數(shù)策略的實(shí)現(xiàn)中茧痒,當(dāng)函數(shù)執(zhí)行完畢后,objA和objB還將繼續(xù)存在融蹂,因?yàn)樗鼈兊囊么螖?shù)永遠(yuǎn)不會是0旺订。
加入這個(gè)函數(shù)被重復(fù)多次調(diào)用,就會導(dǎo)致大量內(nèi)存無法回收超燃。為此区拳,Netscape在Navigator 4.0中也放棄了引用計(jì)數(shù)方式,轉(zhuǎn)而采用標(biāo)記清除來實(shí)現(xiàn)其垃圾回收機(jī)制意乓。
還要注意的是樱调,我們大部分人時(shí)刻都在寫著循環(huán)引用的代碼,看下面這個(gè)例子届良,相信大家都這樣寫過:
var el = document.getElementById('#el');
el.onclick = function (event) {
console.log('element was clicked');
}
我們?yōu)橐粋€(gè)元素的點(diǎn)擊事件綁定了一個(gè)匿名函數(shù)笆凌,我們通過event參數(shù)是可以拿到相應(yīng)元素el的信息的。
大家想想士葫,這是不是就是一個(gè)循環(huán)引用呢乞而?
el有一個(gè)屬性onclick引用了一個(gè)函數(shù)(其實(shí)也是個(gè)對象),函數(shù)里面的參數(shù)又引用了el慢显,這樣el的引用次數(shù)一直是2爪模,即使當(dāng)前這個(gè)頁面關(guān)閉了欠啤,也無法進(jìn)行垃圾回收。
如果這樣的寫法很多很多屋灌,就會造成內(nèi)存泄露洁段。我們可以通過在頁面卸載時(shí)清除事件引用,這樣就可以被回收了:
var el = document.getElementById('#el');
el.onclick = function (event) {
console.log('element was clicked');
}
// ...
// ...
// 頁面卸載時(shí)將綁定的事件清空
window.onbeforeunload = function(){
el.onclick = null;
}
3. V8垃圾回收策略
自動垃圾回收有很多算法声滥,由于不同對象的生存周期不同眉撵,所以無法只用一種回收策略來解決問題,這樣效率會很低落塑。
所以纽疟,V8采用了一種代回收的策略,將內(nèi)存分為兩個(gè)生代:新生代(new generation)和老生代(old generation)憾赁。
新生代中的對象為存活時(shí)間較短的對象污朽,老生代中的對象為存活時(shí)間較長或常駐內(nèi)存的對象,分別對新老生代采用不同的垃圾回收算法來提高效率龙考,對象最開始都會先被分配到新生代(如果新生代內(nèi)存空間不夠蟆肆,直接分配到老生代),新生代中的對象會在滿足某些條件后晦款,被移動到老生代炎功,這個(gè)過程也叫晉升,后面會詳細(xì)說明缓溅。
分代內(nèi)存
默認(rèn)情況下蛇损,32位系統(tǒng)新生代內(nèi)存大小為16MB,老生代內(nèi)存大小為700MB坛怪,64位系統(tǒng)下淤齐,新生代內(nèi)存大小為32MB,老生代內(nèi)存大小為1.4GB袜匿。
新生代平均分成兩塊相等的內(nèi)存空間更啄,叫做semispace,每塊內(nèi)存大小8MB(32位)或16MB(64位)居灯。
新生代
1. 分配方式
新生代存的都是生存周期短的對象祭务,分配內(nèi)存也很容易,只保存一個(gè)指向內(nèi)存空間的指針穆壕,根據(jù)分配對象的大小遞增指針就可以了待牵,當(dāng)存儲空間快要滿時(shí),就進(jìn)行一次垃圾回收喇勋。
2. 算法
新生代采用Scavenge垃圾回收算法,在算法實(shí)現(xiàn)時(shí)主要采用Cheney算法偎行。
Cheney算法將內(nèi)存一分為二川背,叫做semispace贰拿,一塊處于使用狀態(tài),一塊處于閑置狀態(tài)熄云。
處于使用狀態(tài)的semispace稱為From空間膨更,處于閑置狀態(tài)的semispace稱為To空間。
我畫了一套詳細(xì)的流程圖缴允,接下來我會結(jié)合流程圖來詳細(xì)說明Cheney算法是怎么工作的荚守。
垃圾回收在下面我統(tǒng)稱為 GC(Garbage Collection)。
step1. 在From空間中分配了3個(gè)對象A练般、B矗漾、C
step2. GC進(jìn)來判斷對象B沒有其他引用,可以回收薄料,對象A和C依然為活躍對象
step3. 將活躍對象A敞贡、C從From空間復(fù)制到To空間
step4. 清空From空間的全部內(nèi)存
step5. 交換From空間和To空間
step6. 在From空間中又新增了2個(gè)對象D、E
step7. 下一輪GC進(jìn)來發(fā)現(xiàn)對象D沒有引用了摄职,做標(biāo)記
step8. 將活躍對象A誊役、C、E從From空間復(fù)制到To空間
step9. 清空From空間全部內(nèi)存
step10. 繼續(xù)交換From空間和To空間谷市,開始下一輪
通過上面的流程圖蛔垢,我們可以很清楚的看到,進(jìn)行From和To交換迫悠,就是為了讓活躍對象始終保持在一塊semispace中鹏漆,另一塊semispace始終保持空閑的狀態(tài)。
Scavenge由于只復(fù)制存活的對象及皂,并且對于生命周期短的場景存活對象只占少部分甫男,所以它在時(shí)間效率上有優(yōu)異的體現(xiàn)。Scavenge的缺點(diǎn)是只能使用堆內(nèi)存的一半验烧,這是由劃分空間和復(fù)制機(jī)制所決定的板驳。
由于Scavenge是典型的犧牲空間換取時(shí)間的算法,所以無法大規(guī)模的應(yīng)用到所有的垃圾回收中碍拆。但我們可以看到若治,Scavenge非常適合應(yīng)用在新生代中,因?yàn)樾律袑ο蟮纳芷谳^短感混,恰恰適合這個(gè)算法端幼。
3. 晉升
當(dāng)一個(gè)對象經(jīng)過多次復(fù)制仍然存活時(shí),它就會被認(rèn)為是生命周期較長的對象弧满。這種較長生命周期的對象隨后會被移動到老生代中婆跑,采用新的算法進(jìn)行管理。
對象從新生代移動到老生代的過程叫作晉升庭呜。
對象晉升的條件主要有兩個(gè):
對象從From空間復(fù)制到To空間時(shí)滑进,會檢查它的內(nèi)存地址來判斷這個(gè)對象是否已經(jīng)經(jīng)歷過一次Scavenge回收犀忱。如果已經(jīng)經(jīng)歷過了,會將該對象從From空間移動到老生代空間中扶关,如果沒有阴汇,則復(fù)制到To空間〗诨保總結(jié)來說搀庶,如果一個(gè)對象是第二次經(jīng)歷從From空間復(fù)制到To空間,那么這個(gè)對象會被移動到老生代中铜异。
當(dāng)要從From空間復(fù)制一個(gè)對象到To空間時(shí)哥倔,如果To空間已經(jīng)使用了超過25%,則這個(gè)對象直接晉升到老生代中熙掺。設(shè)置25%這個(gè)閾值的原因是當(dāng)這次Scavenge回收完成后未斑,這個(gè)To空間會變?yōu)镕rom空間篇梭,接下來的內(nèi)存分配將在這個(gè)空間中進(jìn)行对室。如果占比過高,會影響后續(xù)的內(nèi)存分配漩仙。
老生代
1. 介紹
在老生代中缆镣,存活對象占較大比重芽突,如果繼續(xù)采用Scavenge算法進(jìn)行管理,就會存在兩個(gè)問題:
由于存活對象較多董瞻,復(fù)制存活對象的效率會很低寞蚌。
采用Scavenge算法會浪費(fèi)一半內(nèi)存,由于老生代所占堆內(nèi)存遠(yuǎn)大于新生代钠糊,所以浪費(fèi)會很嚴(yán)重挟秤。
所以,V8在老生代中主要采用了Mark-Sweep和Mark-Sweep相結(jié)合的方式進(jìn)行垃圾回收抄伍。
2. Mark-Sweep
Mark-Sweep是標(biāo)記清除的意思艘刚,它分為標(biāo)記和清除兩個(gè)階段。
與Scavenge不同截珍,Mark-Sweep并不會將內(nèi)存分為兩份攀甚,所以不存在浪費(fèi)一半空間的行為。Mark-Sweep在標(biāo)記階段遍歷堆內(nèi)存中的所有對象岗喉,并標(biāo)記活著的對象秋度,在隨后的清除階段,只清除沒有被標(biāo)記的對象钱床。
也就是說荚斯,Scavenge只復(fù)制活著的對象,而Mark-Sweep只清除死了的對象【ㄓ担活對象在新生代中只占較少部分拐格,死對象在老生代中只占較少部分僧免,這就是兩種回收方式都能高效處理的原因刑赶。
我們還是通過流程圖來看一下:
step1. 老生代中有對象A、B懂衩、C撞叨、D、E浊洞、F
step2. GC進(jìn)入標(biāo)記階段牵敷,將A、C法希、E標(biāo)記為存活對象
step3. GC進(jìn)入清除階段枷餐,回收掉死亡的B、D苫亦、F對象所占用的內(nèi)存空間
可以看到毛肋,Mark-Sweep最大的問題就是,在進(jìn)行一次清除回收以后屋剑,內(nèi)存空間會出現(xiàn)不連續(xù)的狀態(tài)润匙。這種內(nèi)存碎片會對后續(xù)的內(nèi)存分配造成問題。
如果出現(xiàn)需要分配一個(gè)大內(nèi)存的情況唉匾,由于剩余的碎片空間不足以完成此次分配孕讳,就會提前觸發(fā)垃圾回收,而這次回收是不必要的巍膘。
2. Mark-Compact
為了解決Mark-Sweep的內(nèi)存碎片問題厂财,Mark-Compact就被提出來了。
Mark-Compact是標(biāo)記整理的意思峡懈,是在Mark-Sweep的基礎(chǔ)上演變而來的璃饱。Mark-Compact在標(biāo)記完存活對象以后,會將活著的對象向內(nèi)存空間的一端移動逮诲,移動完成后帜平,直接清理掉邊界外的所有內(nèi)存。如下圖所示:
step1. 老生代中有對象A梅鹦、B裆甩、C、D齐唆、E嗤栓、F(和Mark—Sweep一樣)
[圖片上傳失敗...(image-6e1023-1555941392151)]
step2. GC進(jìn)入標(biāo)記階段,將A、C茉帅、E標(biāo)記為存活對象(和Mark—Sweep一樣)
[圖片上傳失敗...(image-54ff18-1555941392151)]
step3. GC進(jìn)入整理階段叨叙,將所有存活對象向內(nèi)存空間的一側(cè)移動,灰色部分為移動后空出來的空間
[圖片上傳失敗...(image-e1f701-1555941392151)]
step4. GC進(jìn)入清除階段堪澎,將邊界另一側(cè)的內(nèi)存一次性全部回收
[圖片上傳失敗...(image-c2342f-1555941392151)]
3. 兩者結(jié)合
在V8的回收策略中擂错,Mark-Sweep和Mark-Conpact兩者是結(jié)合使用的。
由于Mark-Conpact需要移動對象樱蛤,所以它的執(zhí)行速度不可能很快钮呀,在取舍上,V8主要使用Mark-Sweep昨凡,在空間不足以對從新生代中晉升過來的對象進(jìn)行分配時(shí)爽醋,才使用Mark-Compact。
總結(jié)
V8的垃圾回收機(jī)制分為新生代和老生代便脊。
新生代主要使用Scavenge進(jìn)行管理蚂四,主要實(shí)現(xiàn)是Cheney算法,將內(nèi)存平均分為兩塊哪痰,使用空間叫From遂赠,閑置空間叫To,新對象都先分配到From空間中妒御,在空間快要占滿時(shí)將存活對象復(fù)制到To空間中解愤,然后清空From的內(nèi)存空間,此時(shí)乎莉,調(diào)換From空間和To空間送讲,繼續(xù)進(jìn)行內(nèi)存分配,當(dāng)滿足那兩個(gè)條件時(shí)對象會從新生代晉升到老生代惋啃。
老生代主要采用Mark-Sweep和Mark-Compact算法哼鬓,一個(gè)是標(biāo)記清除,一個(gè)是標(biāo)記整理边灭。兩者不同的地方是异希,Mark-Sweep在垃圾回收后會產(chǎn)生碎片內(nèi)存,而Mark-Compact在清除前會進(jìn)行一步整理绒瘦,將存活對象向一側(cè)移動称簿,隨后清空邊界的另一側(cè)內(nèi)存,這樣空閑的內(nèi)存都是連續(xù)的惰帽,但是帶來的問題就是速度會慢一些憨降。在V8中,老生代是Mark-Sweep和Mark-Compact兩者共同進(jìn)行管理的该酗。
以上就是本文的全部內(nèi)容授药,書寫過程中參考了很多中外文章士嚎,參考書籍包括樸大大的《深入淺出NodeJS》以及《JavaScript高級程序設(shè)計(jì)》等。我們這里并沒有對具體的算法實(shí)現(xiàn)進(jìn)行探討悔叽,感興趣的朋友可以繼續(xù)深入研究一下莱衩。