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的引用。
請看下面範例:
我們知道,IE中有一部分物件並不是原生javascript物件。例如,其中BOM和DOM中的物件就是使用C 以COM (Component Object Model,組件物件模型)物件的形式實現的,而COM物件的垃圾收集機制所採用的就是引用計數策略。因此,即使IE的javascript引擎是使用標記清除策略來實現的,但javascript存取的COM物件仍然是基於引用計數策略的。 換句話說,只要IE中設計COM對象,就會有循環引用的問題。
下面這個簡單的例子,展示了使用COM物件導致的循環引用問題:
パフォーマンスの問題
ガベージ コレクターは定期的に実行され、変数に割り当てられたメモリの量が客観的であれば、リサイクルの作業負荷も非常に大きくなります。この場合、ガベージ コレクションの間隔を決定することが非常に重要な問題になります。ガベージ コレクターが実行される頻度について考えると、IE の悪名高いパフォーマンスの問題を考えずにはいられません。 IE のガベージ コレクターは、メモリ割り当て、具体的には 256 個の変数、4096 個のオブジェクト (または配列) リテラルと配列要素 (スロット)、または 64KB 文字列に基づいて実行されます。上記のしきい値のいずれかに達すると、ガベージ コレクターが実行されます。この実装の問題は、スクリプトにこれだけ多くの変数が含まれている場合、スクリプトはその存続期間中、その多くの変数を維持する可能性が高いことです。その結果、ガベージ コレクターを頻繁に実行する必要がある場合があります。その結果、IE7 のガベージ コレクション ルーチンの最初の書き換えにより、重大なパフォーマンスの問題が発生しました。
IE7 のリリースにより、JavaScript エンジンのガベージ コレクション ルーチンの動作方法が変更されました。ガベージ コレクションをトリガーする変数割り当て、リテラル、および/または配列要素の重要な値は、動的修正に合わせて調整されます。 IE7 のしきい値は、初期化時に IE6 のしきい値と同じになります。ルーチンが割り当てられたメモリの 15% 未満を再利用する場合、変数、リテラル、配列要素のしきい値は 2 倍になります。ルーチンが割り当てられたメモリの 85% を再利用すると、さまざまなしきい値がデフォルト値にリセットされます。この一見単純な調整により、大量の JavaScript を含むページを実行するときの IE のパフォーマンスが大幅に向上します。
実際、ガベージ コレクション プロセスは一部のブラウザでトリガーできますが、これを実行することはお勧めしません。 IE では、window.CollectGarbage() メソッドを呼び出すと、すぐにガベージ コレクションがポイントされます。Opera7 以降では、window.opera.collect() を呼び出すと、ガベージ コレクション ルーチンも開始されます。
メモリを管理する
ガベージ コレクション メカニズムを備えた言語でプログラムを作成すると、開発者は通常、メモリ管理の問題を心配する必要がなくなります。ただし、メモリ管理とガベージ コレクションにおいて JavaScript が直面する問題は、まだ少し異なります。最も重要な問題の 1 つは、通常、Web ブラウザに割り当てられる使用可能なメモリの量が、デスクトップ アプリケーションに割り当てられるメモリの量よりも少ないことです。これの目的は主にセキュリティ上の理由で、JavaScript を実行している Web ページがすべてのシステム メモリを使い果たし、システムがクラッシュするのを防ぐことです。メモリ制限の問題は、変数へのメモリの割り当てに影響を与えるだけでなく、コール スタックやスレッドで同時に実行できるステートメントの数にも影響します。
したがって、ページが占有するメモリ量を最小限に抑えることで、ページのパフォーマンスを向上させることができます。その値を null に設定して参照を解放することが最善です。この方法は逆参照と呼ばれます。このアプローチは、ほとんどのグローバル変数とグローバル オブジェクトのプロパティに使用されます。次の例に示すように、ローカル変数は環境の実行時に自動的に逆参照されます。
// globalperson を手動で逆参照します
globalperson = null;
ただし、値の逆参照は、その値が占有しているメモリを自動的にリサイクルすることを意味するものではありません。逆参照が実際に行うことは、ガベージ コレクターが次回実行するときにその値を再利用できるように、実行環境から値を取り出すことです。
メモリリーク
IE は JScript オブジェクトと COM オブジェクトに対して異なるガベージ コレクション ルーチンを使用するため、クロージャは IE でいくつかの特別な問題を引き起こします。具体的には、HTML 要素がクロージャのスコープ チェーンに格納されている場合、その要素は破棄できないことを意味します。次の例を見てみましょう:
1. そのウィンドウ内のオブジェクトの参照を別のウィンドウに保持すると、ウィンドウを閉じてもメモリは解放されません。
2. さらに悪いことに、DOM オブジェクトへの参照を保持したまま、そのオブジェクトが配置されているウィンドウを閉じると、IE がクラッシュしてメモリ エラーが報告されます (または再起動が必要になります)。