下面這段話引自JavaScript權威指南(第四版)。
因為字符串、對象和數組沒有固定的大小,所以當它們的大小已知時,它們可以被動態分配。每當JavaScript程序創建壹個字符串、數組或對象時,解釋器必須分配內存來存儲該實體。只要內存是這樣動態分配的,最終都會被釋放,以便重用。否則,JavaScript解釋器將消耗系統中所有可用的內存,並導致系統崩潰。
這段話解釋了為什麽系統需要垃圾收集。與C/C++不同,JS有自己的垃圾收集機制。JavaScript的解釋器可以檢測程序何時不再使用對象。當他確定壹個對象無用時,他就知道這個對象不再被需要,可以釋放它所占用的內存。例如:
var a = "之前";
var b = "覆蓋a ";
var a = b;//重寫壹個
這段代碼運行後,字符串“before”失去了它的引用(它以前被壹個。在系統檢測到這壹事實後,它將釋放字符串的存儲空間,以便這些空間可以被重用。
二、垃圾回收的原則
目前各大瀏覽器常用的垃圾收集方式有兩種:標簽清除和引用計數。
1,標記清除
這是javascript中最常用的垃圾收集方法。當變量進入執行環境時,標記為“進入環境”。從邏輯上講,進入環境的變量所占用的內存是永遠無法釋放的,因為只要執行流進入相應的環境,它們就可能被使用。當壹個變量離開環境時,它被標記為“離開環境”。
垃圾收集器在運行時會標記內存中存儲的所有變量。然後,它將刪除環境中的變量以及環境中的變量所引用的標簽。在此之後標記的變量將被視為要刪除的變量,因為環境中的變量不能再訪問它們。最後。垃圾收集器清理內存,銷毀那些標記的值,並回收它們占用的內存空間。
關於這壹塊,我建議看幾篇湯姆叔叔的文章,詳細講解壹些關於作用域鏈的知識。看完之後,妳就差不多知道哪些變量會被標記了。
2.引用計數
另壹種不太常見的垃圾收集策略是引用計數。引用計數的意義是跟蹤和記錄每個值被引用的次數。當壹個變量被聲明並且壹個引用類型被分配給它時,這個值的引用號是1。相反,如果包含對該值的引用的變量獲得另壹個值,對該值的引用數將減少1。當引用次數變成0時,意味著沒有辦法訪問這個值,所以它占用的內存空間是可以回收的。這樣,下壹次垃圾收集器運行時,它將釋放那些引用時間為0的值所占用的內存。
但是這種方法有壹個問題。讓我們看看代碼:
函數問題(){
var objA = new Object();
var objB = new Object();
objA.someOtherObject = objB
objB.anotherObject = objA
}
在這個例子中,objA和objB通過各自的屬性相互引用;也就是說,這兩個對象的引用數是2。在引用計數的策略中,由於函數執行後兩個對象都離開了作用域,所以函數執行後objA和objB會繼續存在,因為它們的引用次數永遠不會為零。如果這種交叉引用大量存在,就會導致大量內存泄漏。
我們知道IE中的壹些對象不是原生的JavaScript對象。比如它的BOM和DOM中的對象都是用COM(組件對象?
模型、組件對象),而com對象的垃圾收集器就是引用計數的策略。所以即使IE的JavaScript引擎用標簽清除的策略實現,Javascript訪問的COM對象仍然是基於引用計數的策略。說白了,IE中只要涉及到COM對象,就會存在循環引用的問題。看看這個簡單的例子:
var element = document . getelementbyid(" some _ element ");
var myObj = new Object();
myObj.element = element
element.someObject = myObj
在上面的例子中,在DOM元素和本地JavaScript對象(myObj)之間建立了壹個循環引用。其中,變量myObj有壹個名為element的屬性指向element;variable元素有壹個名為someObject的屬性,該屬性引用回myObj。由於循環引用,即使示例中的DOM從頁面中移除,內存也不會被回收。
然而,上述問題並非不可解決。我們可以手動切斷它們的循環引用。
myObj.element = null
element.someObject = null
如果這樣寫代碼,可以解決循環引用的問題,也防止了內存泄漏的問題。
第三,減少JavaScript中的垃圾收集
首先,最明顯的是,new關鍵字意味著內存分配,比如new Foo()。最好的方法是在初始化的時候創建新的對象,然後在後續的過程中盡可能的重用這些創建的對象。
還有以下三個內存分配表達式(可能沒有new關鍵字那麽明顯):
{}(創建新對象)
[](創建壹個新數組)
Function() {…}(創建新方法,註意:創建新方法也會導致垃圾回收!!)
1,對象對象優化
為了最大限度地重用對象,我們應該避免使用{}來創建新對象,就像我們避免使用new語句壹樣。
{"foo": "bar"}這種方式的帶屬性的新對象經常被用作方法的返回值,但是這樣會導致內存創建過多,所以最好的解決方法是在每次函數調用完成後,把要返回的數據放到壹個全局對象中,返回這個全局對象。如果使用這個方法,就意味著每次方法調用都會導致全局對象內容的修改,這可能會導致錯誤。因此,必須註意並詳細解釋這個全局對象的使用。
保證對象重用的壹種方法(保證對象原型上沒有屬性)是遍歷這個對象的所有屬性並逐個刪除,最後將對象清理為空對象。
cr.wipe(obj)方法就是為這個函數而生的,代碼如下:?
//刪除obj對象的所有屬性,高效的將obj轉化為壹個全新的對象!
cr.wipe = function (obj) {
for(對象中的變量p){
if (obj.hasOwnProperty(p))
刪除obj[p];
}
};?
有時候可以使用cr.wipe(obj)方法清理對象,然後為obj添加新的屬性,從而達到重用對象的目的。雖然清空壹個對象得到壹個“新對象”需要花費壹些時間,比單純用{}創建壹個對象更耗時,但是在實時性要求高的代碼中,這種短暫的時間消耗會有效的減少垃圾的堆積,最終避免垃圾收集的停頓,非常值得!
2、陣列陣列優化
將[]賦給數組對象是清空數組的捷徑(例如:arr =[];),但是需要註意的是,這個方法創建了壹個新的空對象,把原來的數組對象變成了壹小塊內存垃圾!實際上,將數組長度賦為0(arr.length = 0)也可以達到清空數組的目的,同時可以實現數組重用,減少內存垃圾的產生。
3.方法功能的優化
方法通常是在初始化時創建的,然後很少在運行時進行動態內存分配,這樣就很難找到導致內存垃圾的方法。但從另壹個角度來看,我們更容易發現,因為只要是動態創建方法的地方,就可能產生內存垃圾。例如,將方法作為返回值是動態創建方法的壹個實例。
在遊戲的主循環中,通過setTimeout或requestAnimationFrame調用成員方法是很常見的,例如:
設置超時(
(函數(自身){?
?返回函數(){
?self . tick();
};
})(這個),16)
每隔16毫秒調用this.tick()。嗯,乍壹看好像沒什麽問題,但是仔細壹想,每次調用都返回壹個新的方法對象,導致方法對象垃圾很多!
為了解決這個問題,可以將方法保存為返回值,例如:
//啟動時
this.tickFunc =(
功能(自我){
?返回函數(){
self . tick();
?};
}
)(這個);
//在tick()函數中
setTimeout(this.tickFunc,16);
與每次創建壹個新的方法對象相比,該方法在每壹幀中重用相同的方法對象。這種方法的優點是顯而易見的,這種思想也可以應用於任何以方法作為返回值或者在運行時創建方法的情況。
4.先進技術
基本上,javascript本身是圍繞垃圾收集設計的。隨著我們工作的進展,避免內存垃圾變得越來越困難。因為很多方便實用的Javascript庫方法也會產生壹些新的對象。對於這些庫方法產生的垃圾,我們無能為力,只能查看文檔,檢查方法的返回值。比如數組的slice方法返回壹個新數組(壹部分被截取為新數組,不修改原數組),字符串的substr方法返回壹個新字符串(字符串的壹部分被截取為返回值,不修改原字符串)。
調用這些庫方法會產生內存垃圾,妳能做的就是避免調用這些方法,或者用不產生系統垃圾的方式重寫這些方法(有點極端~)。
例如,在Construct 2引擎中,使用下標從數組中刪除元素是壹種常見的操作。起初,我們通過以下方式認識到這壹點:
var sliced = arr . slice(index+1);
arr.length =索引;
arr.push.apply(arr,sliced);
但是slice方法會返回壹個新的數組對象(數組中的元素從原數組中刪除),通過arr.push.apply方法將元素復制回原數組,但是這個操作之後,數組就變成了壹塊內存垃圾。因為這是我們引擎中垃圾產生的熱代碼(使用非常頻繁),所以我們通過叠代重寫了上面的代碼:
for (var i = index,len = arr . length–1;我& ltleni++)
arr[I]= arr[I+1];
arr .長度= len
顯然,重寫大量的庫函數是非常痛苦的,所以妳必須仔細權衡方法的易用性和內存垃圾的發生。如果在動畫的每壹幀中多次調用產生大量內存垃圾的方法,妳可能很樂意重寫庫函數。
在遞歸函數中,用{}構造壹個空對象,在遞歸過程中傳遞數據是很方便的。但更好的方法是使用單個數組對象作為堆棧,在遞歸過程中對數組執行push和pop操作。再者,不要調用數組的pop方法(pop會讓數組最後壹個元素變成內存垃圾),而是用壹個索引來記錄數組最後壹個元素的位置,在pop的時候簡單的從索引中減壹;類似地,數組的推送操作被索引加1代替。只有當索引的對應元素不存在時,才執行真正的推送,向數組添加新元素。
此外,在任何時候,都應該避免使用vector對象(例如,具有X和Y屬性的vector2對象)。有些方法以vector對象作為方法返回值,不僅可以支持返回值的修改,還可以壹次性返回需要的屬性,使用起來非常方便。但有時壹幀動畫中會創建數百個這樣的矢量對象,導致嚴重的垃圾收集性能問題,這種情況也很常見。因此,最好將這些方法分離成具有獨立職責的功能個體,比如使用getX()和getY()方法(返回特定數據)而不是getPosition()方法(返回壹個vector2對象)。
第四,總結
在Javascript中,完全避免垃圾收集是非常困難的。垃圾收集機制與實時軟件(如遊戲)的實時性要求是根本對立的。
但是,為了減少內存垃圾,我們仍然可以徹底檢查javascript代碼。有些代碼中存在明顯的產生過多內存垃圾的問題代碼,這正是我們需要檢查和改進的地方。
在我看來,只要我們投入更多的精力和註意力,還是有可能實現低垃圾收集的實時javascript應用的。畢竟對於要求高交互性的遊戲或者應用來說,實時性能和低垃圾回收都是至關重要的。