JavaScript 垃圾回收

色拉油發表於2013-10-08

  在公司經常會聽到大牛們討論時說道記憶體洩露神馬的,每每都驚羨不已,最近精力主要用在了Web 開發上,讀了一下《JavaScript高階程式設計》(書名很唬人,實際作者寫的特別好,由淺入深)瞭解了一下JavaScript垃圾回收機制,對記憶體洩露有了一定的認識。

 

  和C#、Java一樣JavaScript有自動垃圾回收機制,也就是說執行環境會負責管理程式碼執行過程中使用的記憶體,在開發過程中就無需考慮記憶體分配及無用記憶體的回收問題了。JavaScript垃圾回收的機制很簡單:找出不再使用的變數,然後釋放掉其佔用的記憶體,但是這個過程不是時時的,因為其開銷比較大,所以垃圾回收器會按照固定的時間間隔週期性的執行。

  變數生命週期

  有同學看了上面就會問了,什麼叫不再使用的變數?不再使用的變數也就是生命週期結束的變數,當然只可能是區域性變數,全域性變數的生命週期直至瀏覽器解除安裝頁面才會結束。區域性變數只在函式的執行過程中存在,而在這個過程中會為區域性變數在棧或堆上分配相應的空間,以儲存它們的值,然後再函式中使用這些變數,直至函式結束(閉包中由於內部函式的原因,外部函式並不能算是結束,瞭解閉包可以看看 JavaScript作用域鏈JavaScript 閉包究竟是什麼)。

  一旦函式結束,區域性變數就沒有存在必要了,可以釋放它們佔用的記憶體。貓和很簡單的工作,為什麼會有很大開銷呢?這僅僅是垃圾回收的冰山一角,就像剛剛提到的閉包,貌似函式結束了,其實還沒有,垃圾回收器必須那個變數游泳,那個變數沒用,對於不再有用的變數打上標記,以備將來回收。用於標記無用的策略有很多,常見的有兩種方式

  標記清除(mark and sweep)

  這是JavaScript最常見的垃圾回收方式,當變數進入執行環境的時候,比如函式中宣告一個變數,垃圾回收器將其標記為“進入環境”,當變數離開環境的時候(函式執行結束)將其標記為“離開環境”。至於怎麼標記有很多種方式,比如特殊位的反轉、維護一個列表等,這些並不重要,重要的是使用什麼策略,原則上講不能夠釋放進入環境的變數所佔的記憶體,它們隨時可能會被呼叫的到。

  垃圾回收器會在執行的時候給儲存在記憶體中的所有變數加上標記,然後去掉環境中的變數以及被環境中變數所引用的變數(閉包),在這些完成之後仍存在標記的就是要刪除的變數了,因為環境中的變數已經無法訪問到這些變數了,然後垃圾回收器相會這些帶有標記的變數機器所佔空間。

  大部分瀏覽器都是使用這種方式進行垃圾回收,區別在於如何標記及垃圾回收間隔而已,只有低版本IE,不出所料,又是IE。。。

  引用計數(reference counting)

  在低版本IE中經常會出現記憶體洩露,很多時候就是因為其採用引用計數方式進行垃圾回收。引用計數的策略是跟蹤記錄每個值被使用的次數,當宣告瞭一個變數並將一個引用型別賦值給該變數的時候這個值的引用次數就加1,如果該變數的值變成了另外一個,則這個值得引用次數減1,當這個值的引用次數變為0的時候,說明沒有變數在使用,這個值沒法被訪問了,因此可以將其佔用的空間回收,這樣垃圾回收器會在執行的時候清理掉引用次數為0的值佔用的空間。

  看起來也不錯的方式,為什麼很少有瀏覽器採用,還會帶來記憶體洩露問題呢?主要是因為這種方式沒辦法解決迴圈引用問題。比如物件A有一個屬性指向物件B,而物件B也有有一個屬性指向物件A,這樣相互引用

function test(){
            var a={};
            var b={};
            a.prop=b;
            b.prop=a;
        }

  這樣a和b的引用次數都是2,即使在test()執行完成後,兩個物件都已經離開環境,在標記清除的策略下是沒有問題的,離開環境的就被清除,但是在引用計數策略下不行,因為這兩個物件的引用次數仍然是2,不會變成0,所以其佔用空間不會被清理,如果這個函式被多次呼叫,這樣就會不斷地有空間不會被回收,造成記憶體洩露。

  在IE中雖然JavaScript物件通過標記清除的方式進行垃圾回收,但BOM與DOM物件卻是通過引用計數回收垃圾的,也就是說只要涉及BOM及DOM就會出現迴圈引用問題。看上面的例子,有同學回覺得太弱了,誰會做這樣無聊的事情,其實我們是不是就在做

window.onload=function outerFunction(){
        var obj = document.getElementById("element");
        obj.onclick=function innerFunction(){};
    };

  這段程式碼看起來沒什麼問題,但是obj引用了document.getElementById(“element”),而document.getElementById(“element”)的onclick方法會引用外部環境中德變數,自然也包括obj,是不是很隱蔽啊。

  解決辦法

  最簡單的方式就是自己手工解除迴圈引用,比如剛才的函式可以這樣

window.onload=function outerFunction(){
        var obj = document.getElementById("element");
        obj.onclick=function innerFunction(){};
       obj=null;
    };

  什麼時候觸發垃圾回收

  垃圾回收器週期性執行,如果分配的記憶體非常多,那麼回收工作也會很艱鉅,確定垃圾回收時間間隔就變成了一個值得思考的問題。IE6的垃圾回收是根據記憶體分配量執行的,當環境中存在256個變數、4096個物件、64k的字串任意一種情況的時候就會觸發垃圾回收器工作,看起來很科學,不用按一段時間就呼叫一次,有時候會沒必要,這樣按需呼叫不是很好嗎?但是如果環境中就是有這麼多變數等一直存在,現在指令碼如此複雜,很正常,那麼結果就是垃圾回收器一直在工作,這樣瀏覽器就沒法兒玩兒了。

  微軟在IE7中做了調整,觸發條件不再是固定的,而是動態修改的,初始值和IE6相同,如果垃圾回收器回收的記憶體分配量低於程式佔用記憶體的15%,說明大部分記憶體不可被回收,設的垃圾回收觸發條件過於敏感,這時候把臨街條件翻倍,如果回收的記憶體高於85%,說明大部分記憶體早就該清理了,這時候把觸發條件置回。這樣就使垃圾回收工作職能了很多。

  同C# 、Java一樣我們可以手工呼叫垃圾回收程式,但是由於其消耗大量資源,而且我們手工呼叫的不會比瀏覽器判斷的準確,所以不推薦手工呼叫垃圾回收。

相關文章