垃圾回收和內(nèi)存管理

JavaScript具有自動(dòng)垃圾收集機(jī)制,也就是說(shuō)執(zhí)行環(huán)境會(huì)負(fù)責(zé)管理代碼執(zhí)行過(guò)程中使用的內(nèi)存。而C和C++之類的語(yǔ)言中,開(kāi)發(fā)人員的一項(xiàng)基本任務(wù)就是手工跟蹤內(nèi)存的使用情況,這是造成許多問(wèn)題的一個(gè)根源。

在編寫(xiě)JavaScript程序時(shí),開(kāi)發(fā)人員不用再關(guān)心內(nèi)存使用問(wèn)題,所需內(nèi)存的分配以及無(wú)用內(nèi)存的回收完全實(shí)現(xiàn)了自動(dòng)管理。這種垃圾收集機(jī)制的原理其實(shí)很簡(jiǎn)單,找出那些不再繼續(xù)使用的變量,然后釋放其占用的內(nèi)存。為此,垃圾收集器會(huì)按照固定的事件間隔(或代碼執(zhí)行中預(yù)定的手機(jī)時(shí)間),周期性地執(zhí)行這一操作。

正因?yàn)槔厥掌鞯拇嬖冢S多人認(rèn)為JavaScript不用太關(guān)心內(nèi)存管理的問(wèn)題。其實(shí)不然,如果不了解JavaScript的垃圾回收機(jī)制,寫(xiě)出來(lái)的代碼會(huì)容易造成內(nèi)存泄漏(內(nèi)存無(wú)法被回收)的情況。所以,想要寫(xiě)出更健壯的代碼,就必須了解JavaScript的垃圾回收機(jī)制。

垃圾回收機(jī)制

內(nèi)存的分配場(chǎng)景
// 對(duì)象
new Object()
new MyConstruction()
{a: 1}
Object.create()

// 數(shù)組
new Array()
[1,2,3]

//字符串 
new String('hello')
'i am string'

//函數(shù)
var fn = function() {}
new Function()

//閉包
function outerFn(name) {
  return function innerFn() {
    return 'Hi' + name
  }
}
內(nèi)存的生命周期

下面分析一下函數(shù)中局部變量的正常生命周期

  • 內(nèi)存分配:局部變量只在函數(shù)執(zhí)行的過(guò)程中存在。而在這個(gè)過(guò)程中,會(huì)為局部變量在棧(或堆)內(nèi)存上分配相應(yīng)的空間,以便存儲(chǔ)它們的值
  • 內(nèi)存使用: 然后在函數(shù)中使用這些變量,直至函數(shù)執(zhí)行結(jié)束
  • 內(nèi)存回收:此時(shí),局部變量就沒(méi)有存在的必要了,因此可以釋放他們的內(nèi)存以供將來(lái)使用
    通常,很容易判斷變量是否還有存在的必要,但并非所有情況都這么容易得出結(jié)論(例如:使用閉包的時(shí)候)。垃圾收集器必須跟蹤哪個(gè)變量沒(méi)用,對(duì)于不再有用的變量打上標(biāo)記,以備將來(lái)收回其占用的內(nèi)存。用于標(biāo)識(shí)無(wú)用變量的策略可能會(huì)因?qū)崿F(xiàn)而異,但具體到瀏覽器中的實(shí)現(xiàn),則通常有兩個(gè)策略:標(biāo)記清除和引用計(jì)數(shù)
標(biāo)記清除

JavaScript中最常用的垃圾收集方式是標(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)記(可使用任何標(biāo)記方式)。然后,它會(huì)去掉環(huán)境中的變量以及被環(huán)境中的變量引用的變量的標(biāo)記(例如,閉包)。而在此之后再被加上標(biāo)記的變量將被視為準(zhǔn)備刪除的變量,原因是環(huán)境中的變量已經(jīng)無(wú)法訪問(wèn)到這些變量了。最后,垃圾回收器完成內(nèi)存清除工作,銷毀那些帶標(biāo)記的值并回收它們所占用的內(nèi)存空間。
這種方式的主要缺點(diǎn)就是如果某些對(duì)象被清理后,內(nèi)存是不連續(xù)的,那么就算內(nèi)存占用率不高,但是由于內(nèi)存空隙太多,后來(lái)的大對(duì)象甚至無(wú)法存儲(chǔ)到內(nèi)存之后。一般的處理方式都是在垃圾回收后進(jìn)行整理操作,這種方法也叫標(biāo)記整理,整理的過(guò)程就是將不連續(xù)的內(nèi)存向一端復(fù)制,使不連續(xù)的內(nèi)存連續(xù)起來(lái)。
目前,IE9+,firefox,Opera,Chrome和Safari的JavaScript實(shí)現(xiàn)的都是標(biāo)記清除式的垃圾收集策略(或類似的策略),只不過(guò)垃圾收集的時(shí)間間隔互有不同

引用計(jì)數(shù)

另一種不太常見(jiàn)的垃圾收集策略叫做引用計(jì)數(shù)。引用計(jì)數(shù)的含義是跟蹤記錄每個(gè)值被引用的次數(shù)。當(dāng)聲明了一個(gè)變量并將一個(gè)引用類型值賦給該變量時(shí),則這個(gè)值的引用次數(shù)就是1。如果同一個(gè)值又被賦給另一個(gè)變量,則該值的引用次數(shù)加1。相反,如果包含對(duì)這個(gè)值引用的變量又取得了另一個(gè)值,則這個(gè)值的引用次數(shù)減1。當(dāng)這個(gè)值的引用次數(shù)變成0時(shí),則說(shuō)明沒(méi)有辦法再訪問(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 
}

早期很多瀏覽器使用引用計(jì)數(shù)策略,但很快它就遇到了一個(gè)嚴(yán)重的問(wèn)題:循環(huán)引用。循環(huán)引用指的是對(duì)象 A 中包含一個(gè)指向?qū)ο?B 的指針,而對(duì)象 B 中也包含一個(gè)指向?qū)ο?A 的引用。請(qǐng)看下面這個(gè)例子:

function problem(){
    var objectA = new Object();
    var objectB = new Object();

    objectA.someOtherObject = objectB;
    objectB.anotherObject = objectA;
}

在這個(gè)例子中,objectA 和 objectB 通過(guò)各自的屬性相互引用;也就是說(shuō),這兩個(gè)對(duì)象的引用次數(shù)都是2。在采用 標(biāo)記清除 策略的實(shí)現(xiàn)中,由于函數(shù)執(zhí)行之后,這兩個(gè)對(duì)象都離開(kāi)了作用域,因此這種相互引用不是個(gè)問(wèn)題。但在采用 引用計(jì)數(shù) 策略的實(shí)現(xiàn)中,當(dāng)函數(shù)執(zhí)行完畢后,objectA 和 objectB 還將繼續(xù)存在,因?yàn)樗鼈兊囊么螖?shù)永遠(yuǎn)不會(huì)是0。假如這個(gè)函數(shù)被重復(fù)多次調(diào)用,就會(huì)導(dǎo)致大量?jī)?nèi)存得不到回收。為此,新一代瀏覽器都放棄了引用計(jì)數(shù)方式,轉(zhuǎn)而采用標(biāo)記清除來(lái)實(shí)現(xiàn)其垃圾收集機(jī)制??墒?,引用計(jì)數(shù)導(dǎo)致的麻煩并未就此終結(jié)。

我們知道,IE 中有一部分對(duì)象并不是原生 JavaScript 對(duì)象。例如,其 BOM 和 DOM 中的對(duì)象就是使用 C++ 以 COM(Component Object Model,組件對(duì)象模型)對(duì)象的形式實(shí)現(xiàn)的,而 COM 對(duì)象的垃圾收集機(jī)制采用的就是引用計(jì)數(shù)策略。因此,即使 IE 的 JavaScript 引擎是使用標(biāo)記清除策略來(lái)實(shí)現(xiàn)的,但 JavaScript 訪問(wèn)的 COM 對(duì)象依然是基于引用計(jì)數(shù)策略的。換句話說(shuō),只要在 IE 中涉及 COM 對(duì)象,就會(huì)存在循環(huán)引用的問(wèn)題。下面這個(gè)簡(jiǎn)單的例子,展示了使用 COM 對(duì)象導(dǎo)致的循環(huán)引用問(wèn)題:

var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject

這個(gè)例子在一個(gè) DOM 元素(element)與一個(gè)原生 JavaScript 對(duì)象(myObject)之間創(chuàng)建了循環(huán)引用。其中,變量 myObject 有一個(gè)名為 element 的屬性指向 element 對(duì)象;而變量 element 也有一個(gè)屬性名叫 someObject 回指 myObject。由于存在這個(gè)循環(huán)引用,即使將例子中的 DOM 從頁(yè)面中移除,它也永遠(yuǎn)不會(huì)被回收。

為了避免類似這樣的循環(huán)引用問(wèn)題,最好是在不使用它們的時(shí)候手工斷開(kāi)原生 JavaScript 對(duì)象與 DOM 元素之間的連接。例如,可以使用下面的代碼消除前面例子創(chuàng)建的循環(huán)引用:

myObject.element = null;
element.someObject = null;

將變量設(shè)置為 null 意味著切斷變量與它此前引用的值之間的連接。當(dāng)垃圾收集器下次運(yùn)行時(shí),就會(huì)刪除這些值并回收它們占用的內(nèi)存。

為了解決上述問(wèn)題,IE9 把 BOM 和 DOM 對(duì)象都轉(zhuǎn)換成了真正的 JavaScript 對(duì)象。這樣,就避免了兩種垃圾收集算法并存導(dǎo)致的問(wèn)題,也消除了常見(jiàn)的內(nèi)存泄漏現(xiàn)象。

IE6的性能問(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ù)雜,那么垃圾回收器會(huì)一直工作,這樣瀏覽器就沒(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í)候則將各種臨界值重置回默認(rèn)值。這一看似簡(jiǎn)單的調(diào)整,極大地提升了 IE7 在運(yùn)行包含大量 JavaScript 的頁(yè)面時(shí)的性能。

編碼注意-解除引用

使用具備垃圾收集機(jī)制的語(yǔ)言編寫(xiě)程序,開(kāi)發(fā)人員一般不必操心內(nèi)存管理的問(wèn)題。但是,JavaScript 在進(jìn)行內(nèi)存管理及垃圾收集時(shí)面臨的問(wèn)題還是有點(diǎn)與眾不同。其中最主要的一個(gè)問(wèn)題,就是分配給 Web 瀏覽器的可用內(nèi)存數(shù)量通常要比分配給桌面應(yīng)用程序的少。這樣做的目的主要是出于安全方面的考慮,目的是防止運(yùn)行 JavaScript 的網(wǎng)頁(yè)耗盡全部系統(tǒng)內(nèi)存而導(dǎo)致系統(tǒng)崩潰。內(nèi)存限制問(wèn)題不僅會(huì)影響給變量分配內(nèi)存,同時(shí)還會(huì)影響調(diào)用棧以及在一個(gè)線程中能夠同時(shí)執(zhí)行的語(yǔ)句數(shù)量。

因此,確保占用最少的內(nèi)存可以讓頁(yè)面獲得更好的性能。而優(yōu)化內(nèi)存占用的最佳方式,就是為執(zhí)行中的代碼只保存必要的數(shù)據(jù)。一旦數(shù)據(jù)不再有用,最好通過(guò)將其值設(shè)置為 null 來(lái)釋放其引用——這個(gè)做法叫做 解除引用(dereferencing)。這一做法適用于大多數(shù)全局變量和全局對(duì)象的屬性。局部變量會(huì)在它們離開(kāi)執(zhí)行環(huán)境時(shí)自動(dòng)被解除引用,如下面這個(gè)例子所示:

function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}

var globalPerson = createPerson("Nicholas");

// 手工解除globalPerson的引用
globalPerson = null;

由于局部變量 localPerson 在 createPerson() 函數(shù)執(zhí)行完畢后就離開(kāi)了其執(zhí)行環(huán)境,因此無(wú)需我們顯式地去為它解除引用。但是對(duì)于全局變量 globalPerson 而言,則需要我們?cè)诓皇褂盟臅r(shí)候手工為它解除引用,這也正是上面例子中最后一行代碼的目的。

不過(guò),解除一個(gè)值的引用并不意味著自動(dòng)回收該值所占用的內(nèi)存。解除引用的真正作用是讓值脫離執(zhí)行環(huán)境,以便垃圾收集器下次運(yùn)行時(shí)將其回收。

垃圾回收的優(yōu)化策略

和其他語(yǔ)言一樣,JavaScript 的垃圾回收策略也無(wú)法避免一個(gè)問(wèn)題:垃圾回收時(shí),會(huì)停止響應(yīng)其他操作,這是為了安全考慮。而 JavaScript 的垃圾回收在 100ms 甚至以上,對(duì)一般的應(yīng)用還好,但對(duì)于 JavaScript 游戲和動(dòng)畫(huà),這種對(duì)連貫性要求比較高的應(yīng)用,就麻煩了。這就是新引擎需要優(yōu)化的點(diǎn):避免垃圾回收造成的長(zhǎng)時(shí)間停止響應(yīng)。

David 大叔主要介紹了2個(gè)優(yōu)化方案,而這也是最主要的2個(gè)優(yōu)化方案了:

分代回收

這個(gè)和Java回收策略思想是一致的。目的是通過(guò)區(qū)分[臨時(shí)]和[持久]對(duì)象;多回收[臨時(shí)對(duì)象區(qū)],少回收[持久對(duì)象區(qū)],減少每次需遍歷的對(duì)象,從而減少每次GC的耗時(shí)。Chorome瀏覽器所使用的V8引擎就是采用的分代回收策略

增量回收

這個(gè)方案的思想很簡(jiǎn)單,就是[每次處理一點(diǎn),下次再處理一點(diǎn),如此類推]。這種方案,雖耗時(shí)短,但中斷較多,帶來(lái)了上下文切換頻繁的問(wèn)題。Firefox 瀏覽器所使用的 JavaScript 引擎就是采用的增量回收策略

因?yàn)槊糠N方案都其適用場(chǎng)景和缺點(diǎn),因此在實(shí)際應(yīng)用中,會(huì)根據(jù)實(shí)際情況選擇方案。例如:如果大量對(duì)象都是長(zhǎng)期「存活」,則分代處理優(yōu)勢(shì)也不大。

查看Chrome瀏覽器下的GC過(guò)程

1.打開(kāi)開(kāi)發(fā)者工具
2.選擇 Performance 面板,勾選memory
3.設(shè)置完成后,點(diǎn)擊最左邊的record按鈕,在頁(yè)面上進(jìn)行各種操作,模擬用戶的使用情況
4.一段時(shí)間后,點(diǎn)擊對(duì)話框的 stop 按鈕,面板上就會(huì)顯示這段時(shí)間的內(nèi)存占用情況
5.然后在chart view上尋找內(nèi)存急速下降的部分,查看對(duì)應(yīng)的event log,可以從中看到GC的日志

文章借鑒

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容