1、垃圾回收的必要性javascript
下面這段話引自《JavaScript權威指南(第四版)》html
因爲字符串、對象和數組沒有固定大小,全部當他們的大小已知時,才能對他們進行動態的存儲分配。JavaScript程序每次建立字符串、數組或對象時,解釋器都必須分配內存來存儲那個實體。只要像這樣動態地分配了內存,最終都要釋放這些內存以便他們可以被再用,不然,JavaScript的解釋器將會消耗完系統中全部可用的內存,形成系統崩潰。java
這段話解釋了爲何須要系統須要垃圾回收,JS不像C/C++,他有本身的一套垃圾回收機制(Garbage Collection)。JavaScript的解釋器能夠檢測到什麼時候程序再也不使用一個對象了,當他肯定了一個對象是無用的時候,他就知道再也不須要這個對象,能夠把它所佔用的內存釋放掉了。例如:數組
var a = "before"; var b = "override a"; var a = b; //重寫a
這段代碼運行以後,「before」這個字符串失去了引用(以前是被a引用),系統檢測到這個事實以後,就會釋放該字符串的存儲空間以便這些空間能夠被再利用。瀏覽器
2、垃圾回收原理淺析app
如今各大瀏覽器一般用採用的垃圾回收有兩種方法:標記清除、引用計數。ide
一、標記清除函數
這是javascript中最經常使用的垃圾回收方式。當變量進入執行環境是,就標記這個變量爲「進入環境」。從邏輯上講,永遠不能釋放進入環境的變量所佔用的內存,由於只要執行流進入相應的環境,就可能會用到他們。當變量離開環境時,則將其標記爲「離開環境」。
垃圾收集器在運行的時候會給存儲在內存中的全部變量都加上標記。而後,它會去掉環境中的變量以及被環境中的變量引用的標記。而在此以後再被加上標記的變量將被視爲準備刪除的變量,緣由是環境中的變量已經沒法訪問到這些變量了。最後。垃圾收集器完成內存清除工做,銷燬那些帶標記的值,並回收他們所佔用的內存空間。性能
關於這一塊,建議讀讀Tom大叔的幾篇文章,關於做用域鏈的一些知識詳解,讀完差很少就知道了,哪些變量會被作標記。
二、引用計數優化
另外一種不太常見的垃圾回收策略是引用計數。引用計數的含義是跟蹤記錄每一個值被引用的次數。當聲明瞭一個變量並將一個引用類型賦值給該變量時,則這個值的引用次數就是1。相反,若是包含對這個值引用的變量又取得了另一個值,則這個值的引用次數就減1。當這個引用次數變成0時,則說明沒有辦法再訪問這個值了,於是就能夠將其所佔的內存空間給收回來。這樣,垃圾收集器下次再運行時,它就會釋放那些引用次數爲0的值所佔的內存。
可是用這種方法存在着一個問題,下面來看看代碼:
function problem() { var objA = new Object(); var objB = new Object(); objA.someOtherObject = objB; objB.anotherObject = objA; }
在這個例子中,objA和objB經過各自的屬性相互引用;也就是說這兩個對象的引用次數都是2。在採用引用計數的策略中,因爲函數執行以後,這兩個對象都離開了做用域,函數執行完成以後,objA和objB還將會繼續存在,由於他們的引用次數永遠不會是0。這樣的相互引用若是說很大量的存在就會致使大量的內存泄露。
咱們知道,IE中有一部分對象並非原生JavaScript對象。例如,其BOM和DOM中的對象就是使用C++以COM(Component Object
Model,組件對象)對象的形式實現的,而COM對象的垃圾回收器就是採用的引用計數的策略。所以,即便IE的Javascript引擎使用標記清除的策略來實現的,但JavaScript訪問的COM對象依然是基於引用計數的策略的。說白了,只要IE中涉及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從頁面中移除,內存也永遠不會回收。
不過上面的問題也不是不能解決,咱們能夠手動切斷他們的循環引用。
myObj.element = null; element.someObject =null;
這樣寫代碼的話就能夠解決循環引用的問題了,也就防止了內存泄露的問題。
3、減小JavaScript中的垃圾回收
首先,最明顯的,new關鍵字就意味着一次內存分配,例如 new Foo()。最好的處理方法是:在初始化的時候新建對象,而後在後續過程當中儘可能多的重用這些建立好的對象。
另外還有如下三種內存分配表達式(可能不像new關鍵字那麼明顯了):
一、對象object優化
爲了最大限度的實現對象的重用,應該像避使用new語句同樣避免使用{}來新建對象。
{「foo」:」bar」}這種方式新建的帶屬性的對象,經常做爲方法的返回值來使用,但是這將會致使過多的內存建立,所以最好的解決辦法是:每一次函數調用完成以後,將須要返回的數據放入一個全局的對象中,並返回此全局對象。若是使用這種方式,就意味着每一次方法調用都會致使全局對象內容的修改,這有可能會致使錯誤的發生。所以,必定要對此全局對象的使用進行詳細的註釋和說明。
有一種方式可以保證對象(確保對象prototype上沒有屬性)的重複利用,那就是遍歷此對象的全部屬性,並逐個刪除,最終將對象清理爲一個空對象。
cr.wipe(obj)方法就是爲此功能而生,代碼以下:
// 刪除obj對象的全部屬性,高效的將obj轉化爲一個嶄新的對象! cr.wipe = function (obj) { for (var p in obj) { if (obj.hasOwnProperty(p)) delete obj[p]; } };
有些時候,你可使用cr.wipe(obj)方法清理對象,再爲obj添加新的屬性,就能夠達到重複利用對象的目的。雖然經過清空一個對象來獲取「新對象」的作法,比簡單的經過{}來建立對象要耗時一些,可是在實時性要求很高的代碼中,這一點短暫的時間消耗,將會有效的減小垃圾堆積,而且最終避免垃圾回收暫停,這是很是值得的!
二、數組array優化
將[]賦值給一個數組對象,是清空數組的捷徑(例如: arr = [];),可是須要注意的是,這種方式又建立了一個新的空對象,而且將原來的數組對象變成了一小片內存垃圾!實際上,將數組長度賦值爲0(arr.length = 0)也能達到清空數組的目的,而且同時能實現數組重用,減小內存垃圾的產生。
三、方法function優化
方法通常都是在初始化的時候建立,而且此後不多在運行時進行動態內存分配,這就使得致使內存垃圾產生的方法,找起來就不是那麼容易了。可是從另外一角度來講,這更便於咱們尋找了,由於只要是動態建立方法的地方,就有可能產生內存垃圾。例如:將方法做爲返回值,就是一個動態建立方法的實例。
在遊戲的主循環中,setTimeout或requestAnimationFrame來調用一個成員方法是很常見的,例如:
setTimeout( (function(self) { return function () { self.tick(); }; })(this), 16)
每過16毫秒調用一次this.tick(),嗯,乍一看彷佛沒什麼問題,可是仔細一琢磨,每一次調用都返回了一個新的方法對象,這就致使了大量的方法對象垃圾!
爲了解決這個問題,能夠將做爲返回值的方法保存起來,例如:
// at startup this.tickFunc = ( function(self) { return function() { self.tick(); }; } )(this); // in the tick() function setTimeout(this.tickFunc, 16);
相比於每次都新建一個方法對象,這種方式在每一幀當中重用了相同的方法對象。這種方式的優點是顯而易見的,而這種思想也能夠應用在任何以方法爲返回值或者在運行時建立方法的狀況當中。
四、高級技術
從根本上來講,javascript自己就是圍繞着垃圾收集來設計的。隨着咱們工做的進行,避免內存垃圾變得愈來愈困難。由於不少方便實用的Javascript庫方法也會產生一些新的對象。對於這些庫方法產生的垃圾,咱們一籌莫展,只能從新翻看文檔,而且檢查方法的返回值。例如,數組的slice方法返回一個新的數組(在不修改原數組的基礎上,截取出一部分做爲新數組),字符串的substr方法返回一個新的字符串(在不修改原字符串的基礎上,截取出一部分字符串做爲返回值)等等。
調用這些庫方法,將會建立內存垃圾,而你能作的,只有避免調用這些方法,或者用不建立系統垃圾的方式重寫這些方法(有點極端啦~)。
例如,在Construct 2引擎中,從數組中利用下標來刪除一個元素,是常常進行的操做。最初咱們是用下面這種方式來實現的:
var sliced = arr.slice(index + 1); arr.length = index; arr.push.apply(arr, sliced);
然而,slice方法會返回一個新的數組對象(數組中的元素是原數組中刪掉的部分),而且會經過arr.push.apply方法將元素從新複製回原數組,可是在此操做以後,該數組就成爲了一片內存垃圾。因爲這是咱們引擎中的垃圾產生的熱點代碼(使用頻率很是很高),所以咱們利用了迭代的方式重寫了上述代碼:
for (var i = index, len = arr.length – 1; i < len; i++) arr[i] = arr[i + 1]; arr.length = len;
顯然,重寫大量的庫函數是很是痛苦的,所以你必須仔細權衡方法的易用性和內存垃圾產生狀況。若是產生大量內存垃圾的方法在動畫的每一幀中被屢次調用,你可能就會興高采烈的重寫庫函數啦。
在遞歸函數中,經過{}構造空對象,並在遞歸過程當中傳遞數據,雖然是很方便的。可是更好的方式是:利用一個單獨的數組對象做爲堆棧,在遞歸過程當中對數組進行push和pop操做。更進一步,不要調用array的pop方法(pop將會使得array的最後一個元素將會變成內存垃圾),而應該使用一個索引來記錄數組的最後一個元素的位置,在pop時簡單的將索引減一便可;相似的,將索引加1來代替array的push操做,只有當索引對應的元素不存在時,才執行真正的push爲數組加入一個新元素。
另外,在任什麼時候候,都應該避免使用向量對象(例如:包含x和y屬性的vector2對象)。有些方法將向量對象做爲方法返回值,既能夠支持返回值的再次修改,又可以將須要的屬性一次性返回,使用起來很是方便。可是有時候在一幀動畫中,建立了成百上千個這樣的向量對象,從而致使嚴重的垃圾回收性能問題,也是很是常見的。所以最好將這些方法分離成具備獨立職責的功能個體,例如:利用getX()和getY()方法(返回具體數據)代替getPosition()方法(返回一個vector2對象)。
4、總結
在Javascript中,完全避免垃圾回收是很是困難的。垃圾回收機制與實時軟件(例如:遊戲)的實時性要求,從根本上就是對立的。
可是,爲了減小內存垃圾,咱們仍是能夠對javascript代碼進行完全檢查,有些代碼中存在明顯的產生過多內存垃圾的問題代碼,這些正是咱們須要檢查而且完善的。
我認爲,只要咱們投入更多的精力和關注,實現實時的、低垃圾收集的javascript應用仍是頗有可能的。畢竟,對於可交互性要求較高的遊戲或應用來講,實時性和低垃圾收集,二者都是相當重要。