??JavaScript具有自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程中使用的內存。而在C和C++之類的語言中,開發人員的一項基本任務就是手工跟蹤內存的使用情況,這是造成許多問題的一個根源。在編寫JavaScript程序時,開發人員不用再關心內存使用問題,所需內存的分配以及無用內存的回收完全實現了自動管理。這種垃圾收集機制的原理其實很簡單:找出那些不再繼續使用的變量,然后釋放其占用的內存。為此,垃圾收集器會按照固定的時間間隔(或代碼執行中預定的收集時間),周期性地執行這一操作。
??下面我們來分析一下函數中局部變量的正常生命周期。局部變量只在函數執行的過程中存在。而在這個過程中,會為局部變量在棧(或堆)內存上分配相應的空間,以便存儲它們的值。然后在函數中使用這些變量,直至函數執行結束。此時,局部變量就沒有存在的必要了,因此可以釋放它們的內存以供將來使用。在這種情況下,很容易判斷變量是否還有存在的必要;但并非所有情況下都這么容易就能得出結論。垃圾收集器必須跟蹤哪個變量有用哪個變量沒用,對于不再有用的變量打上標記,以備將來收回其占用的內存。用于標識無用變量的策略可能會因實現而異,但具體到瀏覽器中的實現,則通常有兩個策略。
標記清除##
??JavaScript中最常用的垃圾收集方式是標記清除(mark-and-sweep)。當變量進入環境(例如,在函數中聲明一個變量)時,就將這個變量標記為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變量所占用的內存,因為只要執行流進入相應的環境,就可能會用到它們。而當變量離開環境時,則將其標記為“離開環境”。
??可以使用任何方式來標記變量。比如,可以通過翻轉某個特殊的位來記錄一個變量何時進入環境,或者使用一個“進入環境的”變量列表及一個“離開環境的”變量列表來跟蹤哪個變量發生了變化。說到底,如何標記變量其實并不重要,關鍵在于采取什么策略。
??垃圾收集器在運行的時候會給存儲在內存中的所有變量都加上標記(當然,可以使用任何標記方式)。然后,它會去掉環境中的變量以及被環境中的變量引用的變量的標記。而在此之后再被加上標記的變量將被視為準備刪除的變量,原因是環境中的變量已經無法訪問到這些變量了。最后,垃圾收集器完成內存清除工作,銷毀那些帶標記的值并回收它們所占用的內存空間。
??到2008年為止,IE、Firefox、Opera、Chrome和Safari的JavaScript實現使用的都是標記清除式的垃圾收集策略(或類似的策略),只不過垃圾收集的時間間隔互有不同。
引用計數##
??另一種不太常見的垃圾收集策略叫做引用計數(reference counting)。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明了一個變量并將一個引用類型值賦給該變量時,則這個值的引用次數就是1。如果同一個值又被賦給另一個變量,則該值的引用次數加1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的內存空間回收回來。這樣,當垃圾收集器下次再運行時,它就會釋放那些引用次數為零的值所占用的內存。
??Netscape Navigator 3.0是最早使用引用計數策略的瀏覽器,但很快它就遇到了一個嚴重的問題:循環引用。循環引用指的是對象A中包含一個指向對象B的指針,而對象B中也包含一個指向對象A的引用。請看下面這個例子:
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
??在這個例子中,objectA 和 objectB 通過各自的屬性相互引用;也就是說,這兩個對象的引用次 數都是 2。在采用標記清除策略的實現中,由于函數執行之后,這兩個對象都離開了作用域,因此這種 相互引用不是個問題。但在采用引用計數策略的實現中,當函數執行完畢后,objectA 和 objectB 還 將繼續存在,因為它們的引用次數永遠不會是 0。假如這個函數被重復多次調用,就會導致大量內存得 不到回收。為此,Netscape 在 Navigator 4.0 中放棄了引用計數方式,轉而采用標記清除來實現其垃圾收 集機制。可是,引用計數導致的麻煩并未就此終結。
??我們知道,IE 中有一部分對象并不是原生 JavaScript 對象。例如,其 BOM 和 DOM 中的對象就是使用 C++以COM(Component Object Model,組件對象模型)對象的形式實現的,而 COM 對象的垃圾 收集機制采用的就是引用計數策略。因此,即使 IE 的 JavaScript 引擎是使用標記清除策略來實現的,但 JavaScript 訪問的 COM 對象依然是基于引用計數策略的。換句話說,只要在 IE 中涉及 COM 對象,就會 存在循環引用的問題。下面這個簡單的例子,展示了使用 COM 對象導致的循環引用問題:
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;
??這個例子在一個 DOM 元素(element)與一個原生 JavaScript 對象(myObject)之間創建了循環 引用。其中,變量 myObject 有一個名為 element 的屬性指向 element 對象;而變量 element 也有 一個屬性名叫 someObject 回指 myObject。由于存在這個循環引用,即使將例子中的 DOM 從頁面中 移除,它也永遠不會被回收。
??為了避免類似這樣的循環引用問題,最好是在不使用它們的時候手工斷開原生 JavaScript 對象與 DOM 元素之間的連接。例如,可以使用下面的代碼消除前面例子創建的循環引用:
myObject.element = null;
element.someObject = null;
??將變量設置為 null 意味著切斷變量與它此前引用的值之間的連接。當垃圾收集器下次運行時,就 會刪除這些值并回收它們占用的內存。
??為了解決上述問題,IE9 把 BOM 和 DOM 對象都轉換成了真正的 JavaScript 對象。這樣,就避免了 兩種垃圾收集算法并存導致的問題,也消除了常見的內存泄漏現象。
<<JavaScript高級程序設計>>