一、垃圾收集原理與意義 在C和C++之類的語言中,開發人員的一項基本任務就是手動跟蹤記憶體的使用情況,這是造成很多問題的根源。Javascript具有垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程中使用的記憶體。在編寫JavaScript程式時,開發人員不再關心記憶體使用問題,所需記憶體的分配以及無 ...
一、垃圾收集原理與意義
在C和C++之類的語言中,開發人員的一項基本任務就是手動跟蹤記憶體的使用情況,這是造成很多問題的根源。Javascript具有垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程中使用的記憶體。在編寫JavaScript程式時,開發人員不再關心記憶體使用問題,所需記憶體的分配以及無用記憶體的回收完全實現了自動管理。這種垃圾收集機制的原理很簡單:找出那些不再繼續使用的變數,然後釋放其占用的記憶體。為此,垃圾收集器會按照固定時間間隔(或代碼執行中預定的收集時間),周期性地執行這一操作。
局部變數只在函數執行過程中存在。而在這個過程中,會為局部變數在棧(或堆)記憶體上分配相應的空間,以便存儲它們的值。然後在函數中使用這些變數,直至函數執行結束。此時,局部變數就沒有存在的必要了,因此可以釋放他們的記憶體以供將來使用。在這種情況下,很容易判斷變數是否還有存在的必要;但並非所有情況下就這麼容易得出結論。垃圾收集器必須跟蹤哪個變數有用哪個變數沒用,對於不再有用的對象打上標記,以備將來回收其占用的記憶體。
二、垃圾收集策略
現在各大瀏覽器通常採用的垃圾回收策略有兩種:標記清除和引用計數。
1. 標記清除
JavaScript最常用的垃圾收集方式就是標記清除(mark-and-sweep)。當變數進入環境時,就將該變數標記為"進入環境"。從邏輯上講,永遠不能釋放進入環境的變數所占用的記憶體,因為只要執行流進入到相應的環境,就可能會用到它們。而當變數離開環境時,則將其標記為”離開環境“。
垃圾收集器在運行的時候會給存儲在記憶體上的所有變數都加上標記。然後,它會去掉環境中的變數以及被環境中的變數引用的變數的標記。而在此之後再被加上標記的變數將被視為準備刪除的變數,原因是環境中的變數已經無法訪問到這些變數了。最後,垃圾收集器完成記憶體清除工作,銷毀那些帶標記的值並回收他們所占用的記憶體空間。
2. 引用計數
這一種垃圾收集策略不太常用,原因在於很容易造成嚴重的問題:迴圈引用。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明瞭一個變數並將引用類型值賦給該變數時,則這個值得引用次數就是1。如果同一個值又被賦給另一個變數,則該值的引用次數加1。相反如果包含對這個值引用的變數又取到了另外一個值,則這個值的引用次數減1。當這個值的引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其占用的記憶體空間回收回來。這樣當垃圾收集器下次再運行時,它就會釋放那些引用次數為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。假如這個函數被重覆多次調用,就會導致大量的記憶體得不到回收。
在IE9之前版本中,有一部分對象並不是原生的Javascript對象。例如,其BOM和DOM中的對象就是使用C++以COM對象的形式實現的,而COM對象的垃圾收集機制採用的就是引用計數策略。因此,即使IE的Javascrip引擎是使用標記清除策略實現的,但Javascript訪問的COM對象依然是基於引用計數策略的。
var element = document.getElementById("some_element"); var myObj =new Object(); myObj.element = element; element.someObject = myObj;
上面這個例子中,在一個DOM元素(element)與一個原生JavaScript對象(myObj)之間建立了迴圈引用。其中,變數myObj有一個名為element的屬性指向element;而變數element有一個名為someObject的屬性回指到myObj。由於迴圈引用,即使將例子中的DOM從頁面中移除,記憶體也永遠不會回收 。
為避免這類問題,最好在不使用它們的時候手工斷開原生Javascript與DOM元素之間的連接:
myObject.element = null; element.someObject = null;
將變數設為null意味著切斷變數與它之前引用的值之間的連接。當垃圾收集器下次運行的時候,就會刪除這些值並回收它們所占用的記憶體。IE9+把BOM和DOM對象都轉換成了真正的Javascript對象。這樣就消除了常見的記憶體泄露現象。
三、管理記憶體
1. 觸發垃圾收集
IE的垃圾收集是根據記憶體分配量進行的,具體就是256個變數、4096個對象(或數組)字面量和數組元素(slot)或則64KB的字元串。達到上述的任何一個臨界值,垃圾收集器就會運行。這種實現方式的問題在於,如果一個腳本中包含那麼多變數,那麼該腳本很可能會在其生命周期中一直保存著那麼多的變數。而這樣一來,垃圾收集器就不得不頻繁地運行,造成嚴重的性能問題。IE7之後,其Javascript引擎改變了其垃圾收集常式:觸發垃圾收集的變數分配、字面量和數組元素的臨界值被調整為動態修正。IE7的各項臨界初始值和IE6相同,如果垃圾收集常式回收的記憶體分配量低於15%,這時候把臨界條件(變數、字面量、數組元素)加倍,如果常式回收了85%的記憶體分配量,則將各種臨界值重置回預設值。
2. 管理記憶體
Javascript在進行記憶體管理時最主要的問題就是分配給Web瀏覽器的可用記憶體量通常要比分配給桌面應的少。這樣做的目的是出於安全方面的考慮,目的是防止運行Javascript的網頁耗盡全部系統記憶體而導致系統崩潰。記憶體限制問題不僅會影響給變數分配的記憶體,同時還會影響調用棧以及在一個線程中能夠同時執行的語句數量。
因此,確保占有最少的記憶體可以讓頁面獲得更好的性能。而優化記憶體占用的最佳方式,就是為執行中的代碼只保留必要的數據。一旦數據不再有用,最好通過將其值設置為null來解除其引用。這一做法適用於大多數全局變數和全局對象的屬性。局部變數會在它們離開執行環境時自動被解除引用。
function createPerson(name) { var localPerson = new Object(); localPerson.name = name; return localPerson; } var globalPerson = createPerson("Nicholas"); globalPerson = null;
在這個例子中,由於localPerson在createPerson()函數執行完畢後就離開了其執行環境,因此無需顯示地為它解除引用。但對於全局變數globalPerson,則需要我們在不使用它的時候手工為它解除引用。不過,解除一個值得引用並不意味著自動回收該值所占用的記憶體。解除引用真正的作用是讓值脫離執行環境,以便垃圾收集器下次運行時將其回收。
四、減少垃圾收集
1. 對象object優化
為了最大限度的實現對象的重用,應該像避免使用new語句一樣避免使用{}來新建對象。
{“foo”:”bar”}這種方式新建的帶屬性的對象,常常作為方法的返回值來使用,可是這將會導致過多的記憶體創建,因此最好的解決辦法是:每一次函數調用完成之後,將需要返回的數據放入一個全局的對象中,並返回此全局對象。如果使用這種方式,就意味著每一次方法調用都會導致全局對象內容的修改,這有可能會導致錯誤的發生。因此,一定要對此全局對象的使用進行詳細的註釋和說明。
有一種方式能夠保證對象(確保對象prototype上沒有屬性)的重覆利用,那就是遍歷此對象的所有屬性,並逐個刪除,最終將對象清理為一個空對象。
cr.wipe(obj)方法就是為此功能而生,代碼如下:
cr.wipe = function (obj) { for (var p in obj) { if (obj.hasOwnProperty(p)) delete obj[p]; } };
有些時候,你可以使用cr.wipe(obj)方法清理對象,再為obj添加新的屬性,就可以達到重覆利用對象的目的。雖然通過清空一個對象來獲取“新對象”的做法,比簡單的通過{}來創建對象要耗時一些,但是在實時性要求很高的代碼中,這一點短暫的時間消耗,將會有效的減少垃圾堆積,並且最終避免垃圾回收暫停,這是非常值得的!
2. 數組優化
將[]賦值給一個數組對象,是清空數組的捷徑(例如: arr = [];),但是需要註意的是,這種方式又創建了一個新的空對象,並且將原來的數組對象變成了一小片記憶體垃圾!實際上,將數組長度賦值為0(arr.length = 0)也能達到清空數組的目的,並且同時能實現數組重用,減少記憶體垃圾的產生。
3. function優化
方法一般都是在初始化的時候創建,並且此後很少在運行時進行動態記憶體分配,這就使得導致記憶體垃圾產生的方法,找起來就不是那麼容易了。但是從另一角度來說,這更便於我們尋找了,因為只要是動態創建方法的地方,就有可能產生記憶體垃圾。例如:將方法作為返回值,就是一個動態創建方法的實例。
setTimeout( (function(self) { return function () { self.tick(); }; })(this), 100)
每過100毫秒調用一次this.tick(),嗯,乍一看似乎沒什麼問題,但是仔細一琢磨,每一次調用都返回了一個新的方法對象,這就導致了大量的方法對象垃圾!
為瞭解決這個問題,可以將作為返回值的方法保存起來,例如:
this.tickFunc = ( function(self) { return function() { self.tick(); }; } )(this); setTimeout(this.tickFunc, 100);
參考:
Javascript高級程式設計
https://www.cnblogs.com/zhwl/p/4664604.html