一、引言
JavaScript具有自動垃圾收集機制悴了,即執(zhí)行環(huán)境會負(fù)責(zé)管理代碼執(zhí)行過程中使用的內(nèi)存煞肾。
垃圾收集機制的原理:垃圾回收機制就是垃圾收集器周期性地找到不再使用的變量,并釋放掉它們所指向的內(nèi)存囤捻。
JS的垃圾回收機制是為了以防內(nèi)存泄漏。不再用到的內(nèi)存邻寿,沒有及時釋放蝎土,就叫做內(nèi)存泄漏视哑,即當(dāng)已經(jīng)不需要某塊內(nèi)存時這塊內(nèi)存還存在著。通俗點說就是誊涯,這個內(nèi)存該清掉挡毅,但是沒有被清掉,就造成了內(nèi)存泄露暴构。
程序的運行需要內(nèi)存跪呈,只要程序提出要求,操作系統(tǒng)就必須供給內(nèi)存丹壕。對于持續(xù)運行的服務(wù)進(jìn)程庆械,必須及時釋放內(nèi)存,否則菌赖,內(nèi)存占用越來越高缭乘,輕則影響系統(tǒng)性能,重則導(dǎo)致進(jìn)程崩潰琉用。
通常有兩個用于標(biāo)識無用變量的策略(垃圾收集方式):標(biāo)記清除堕绩、引用計數(shù)。
三邑时、標(biāo)記清除(js中最常用的垃圾收集方式)
當(dāng)變量進(jìn)入環(huán)境(如在函數(shù)中聲明一個變量)奴紧,就標(biāo)記這個變量為“進(jìn)入環(huán)境”。當(dāng)變量離開環(huán)境晶丘,則將其標(biāo)記為“離開環(huán)境”黍氮。
標(biāo)記清除:垃圾收集器先給存儲在內(nèi)存中的所有對象加上標(biāo)記,然后去掉環(huán)境中的變量和被環(huán)境中的變量引用的對象的標(biāo)記浅浮,剩下的被標(biāo)記的對象就被視為準(zhǔn)備刪除的變量沫浆,原因是環(huán)境中的變量已經(jīng)無法訪問到這些變量了。最后滚秩。垃圾收集器完成內(nèi)存清除工作专执,銷毀那些帶標(biāo)記的值,并回收他們所占用的內(nèi)存空間郁油。
二本股、引用計數(shù)
引用計數(shù)的含義是跟蹤記錄每個值被引用的次數(shù)。
引用計數(shù):當(dāng)聲明了一個變量并將一個引用類型值賦給該變量時桐腌,則這個值的引用次數(shù)就是1拄显。如果同一個值又被賦給另一個變量,則該值的引用次數(shù)加1案站。相反躬审,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數(shù)就減1。當(dāng)這個引用次數(shù)變成0時盒件,則說明沒有辦法再訪問這個值了,因而就可以將其所占的內(nèi)存空間給收回來舱禽。這樣炒刁,垃圾收集器下次再運行時,它就會釋放那些引用次數(shù)為0的值所占的內(nèi)存誊稚。
但是當(dāng)對象循環(huán)引用時翔始,會導(dǎo)致引用次數(shù)永遠(yuǎn)無法歸零,造成內(nèi)存無法釋放里伯。循環(huán)引用指的是:對象A中包含一個指向?qū)ο驜的指針城瞎,而對象B中也包含一個指向?qū)ο驛的引用,例如:
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anOtherObject = objectA;
}
// 對象A和B的引用次數(shù)都是2疾瓮,內(nèi)存得不到回收脖镀。
由于低版本IE中的BOM和DOM的對象就是使用C++以COM(Component Object Model,組件對象模型)對象的形式實現(xiàn)的狼电,而COM對象的垃圾收集機制采用的就是引用計數(shù)策略蜒灰。 因此,即使IE的JavaScript引擎是使用標(biāo)記清除策略來實現(xiàn)的肩碟,但JavaScript訪問的COM對象依然是基于引用計數(shù)策略的强窖。換句話說,只要在IE中涉及COM對象削祈,就會存在循環(huán)引用的問題翅溺。
為了解決這個問題,IE9把BOM和DOM對象都轉(zhuǎn)換成了真正的JavaScript對象髓抑,就避免了兩種垃圾收集算法并存導(dǎo)致的問題咙崎,也消除了常見的內(nèi)存泄露現(xiàn)象。
三启昧、V8如何進(jìn)行垃圾回收
[圖片上傳失敗...(image-2c0212-1597281228800)]
棧內(nèi)存的回收:
棧內(nèi)存調(diào)用棧上下文切換后就被回收叙凡,比較簡單。
函數(shù)執(zhí)行完密末,形成的執(zhí)行上下文中握爷,沒有東西被上下文以外的內(nèi)容占用,此上下文就會從執(zhí)行環(huán)境棧中移除(釋放)严里,如果有被占用新啼,則壓縮到棧的底部(沒有釋放,就形成閉包)刹碾。
堆內(nèi)存的回收:變量 = null
V8的堆內(nèi)存分為新生代內(nèi)存和老生代內(nèi)存燥撞,新生代內(nèi)存是臨時分配的內(nèi)存,存在時間短,老生代內(nèi)存存在時間長物舒。
新生代內(nèi)存回收機制:
- 新生代內(nèi)存容量小色洞,64位系統(tǒng)下僅有32M。新生代內(nèi)存分為From冠胯、To兩部分火诸,進(jìn)行垃圾回收時,先掃描From荠察,將非存活對象回收置蜀,將存活對象順序復(fù)制到To中,之后調(diào)換From/To悉盆,等待下一次回收
老生代內(nèi)存回收機制
- 晉升:如果新生代的變量經(jīng)過多次回收依然存在盯荤,那么就會被放入老生代內(nèi)存中
- 標(biāo)記清除:老生代內(nèi)存會先遍歷所有對象并打上標(biāo)記,然后對正在使用或被強引用的對象取消標(biāo)記焕盟,回收被標(biāo)記的對象
- 整理內(nèi)存碎片:把對象挪到內(nèi)存的一端
四秋秤、管理內(nèi)存
確保占用最少的內(nèi)存可以讓頁面獲得更好的性能。而優(yōu)化內(nèi)存占用的最佳方式脚翘,就是為執(zhí)行中的代碼只保存必要的數(shù)據(jù)航缀。一旦數(shù)據(jù)不再有用,最好通過將其值設(shè)置為null來釋放其引用(解除引用)堰怨,這一做法適用于大多數(shù)全局變量和全局對象的屬性芥玉。解除引用的真正作用是讓值脫離執(zhí)行環(huán)境,以便垃圾收集器下次運行時將其回收(并不意味著解除就自動回收該值所占用的內(nèi)存)备图。
function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson('Shine')灿巧;
// 手工解除全局變量globalPerson的引用
globalPerson = null;
五揽涮、垃圾收集與閉包
閉包可以避免全局變量的污染抠藕,但是如果閉包使用過多,就會使得很多局部變量常駐內(nèi)存蒋困,增加了內(nèi)存的開銷盾似。濫用閉包在IE中可能會造成內(nèi)存泄露。
沒有產(chǎn)生閉包的情況:
當(dāng)fn()
執(zhí)行完之后雪标,函數(shù)內(nèi)部的局部變量a以及局部函數(shù)就銷毀了零院。則每執(zhí)行一次,局部變量和局部函數(shù)都是重新定義的村刨,執(zhí)行完畢后告抄,就會被垃圾收集器回收。
即沒有閉包的情況下嵌牺,執(zhí)行完fn()
打洼,a
會自動釋放龄糊。
function fn(){
var a = 1;
return function(){
return a++;
}
}
console.log(fn()); // ? () { return a++; }
console.log(fn()()); // 1
console.log(fn()()); // 1
產(chǎn)生閉包的情況:
fn函數(shù)每次執(zhí)行,都會形成一個新的環(huán)境募疮,這個新的環(huán)境被全局變量test
保存下來炫惩,test
和子函數(shù)建立了引用關(guān)系,子函數(shù)和父函數(shù)中的局部變量又存在引用關(guān)系阿浓。
由于兩個以上存在引用關(guān)系的對象诡必,只要有一個是全局的,那么其他的就不會被回收搔扁。由于test
是全局的,因此a
不會被釋放蟋字。
function fn(){
var a = 1;
return function(){
return a++;
}
}
// 在父函數(shù)的外部稿蹲,調(diào)用其局部變量,聲明一個全局變量test來接收父函數(shù)執(zhí)行后返回的匿名函數(shù)
var test = fn();
console.log(test); // ? () { return a++; }
console.log(test()); // 1
console.log(test()); // 2鹊奖, fn每次執(zhí)行都會形成一個新的環(huán)境苛聘,也稱為閉包環(huán)境
當(dāng)包含閉包的對象成為垃圾對象,即失去引用test = null
忠聚,閉包中涉及的變量a
再也沒有被引用设哗,閉包死亡。
解除引用后两蟀,就會讓a
脫離執(zhí)行環(huán)境网梢,以便垃圾收集器下次運行時將其回收。