1、what? 垃圾回收:js代碼想要運行,需要操作系統或者運行時提供記憶體空間,來存儲變數及它的值。在某些變數(例如局部變數)在不參與運行時,就需要系統回收被占用的記憶體空間,稱為垃圾回收 記憶體泄漏:某些情況下,不再用到的變數所占記憶體沒有及時釋放,導致程式運行中,記憶體越占越大,極端情況下可導致系統崩潰 ...
1、what?
垃圾回收:js代碼想要運行,需要操作系統或者運行時提供記憶體空間,來存儲變數及它的值。在某些變數(例如局部變數)在不參與運行時,就需要系統回收被占用的記憶體空間,稱為垃圾回收
記憶體泄漏:某些情況下,不再用到的變數所占記憶體沒有及時釋放,導致程式運行中,記憶體越占越大,極端情況下可導致系統崩潰、伺服器宕機。
在C與C++等語言中,開發人員可以直接控制記憶體的申請和回收。但是在Java、C#、JavaScript語言中,變數的記憶體空間的申請和釋放都由程式自己處理,開發人員不需要關心。也就是說Javascript具有自動垃圾回收機制(GC:Garbage Collecation)。
JavaScript垃圾回收的機制很簡單:找出不再使用的變數,然後釋放掉其占用的記憶體
2、why?
由於字元串、對象和數組沒有固定大小,所有當他們的大小已知時,才能對他們進行動態的存儲分配。JavaScript程式每次創建字元串、數組或對象時,解釋器都必須分配記憶體來存儲那個實體。只要像這樣動態地分配了記憶體,最終都要釋放這些記憶體以便他們能夠被再用,否則,JavaScript的解釋器將會消耗完系統中所有可用的記憶體,造成系統崩潰。
---------《JavaScript權威指南(第四版)》
3、when?
垃圾回收器周期性運行,如果分配的記憶體非常多,那麼回收工作也會很艱巨,確定垃圾回收時間間隔就變成了一個值得思考的問題。
IE6的垃圾回收是根據記憶體分配量運行的,當環境中存在256個變數、4096個對象、64k的字元串任意一種情況的時候就會觸發垃圾回收器工作,看起來很科學,不用按一段時間就調用一次,有時候會沒必要,這樣按需調用不是很好嗎?但是如果環境中就是有這麼多變數等一直存在,現在腳本如此複雜,很正常,那麼結果就是垃圾回收器一直在工作,這樣瀏覽器就沒法兒玩兒了。
微軟在IE7中做了調整,觸發條件不再是固定的,而是動態修改的,初始值和IE6相同,如果垃圾回收器回收的記憶體分配量低於程式占用記憶體的15%,說明大部分記憶體不可被回收,設的垃圾回收觸發條件過於敏感,這時候把臨界條件翻倍,如果回收的記憶體高於85%,說明大部分記憶體早就該清理了,這時候把觸發條件置回。這樣就使垃圾回收工作職能了很多。
同C# 、Java一樣我們可以手工調用垃圾回收程式,但是由於其消耗大量資源,而且我們手工調用的不會比瀏覽器判斷的準確,所以不推薦手工調用垃圾回收。
4、how?
現在各大瀏覽器通常用採用的垃圾回收有兩種方法:標記清除、引用計數。
1、引用計數
另一種不太常見的垃圾回收策略是引用計數。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明瞭一個變數並將一個引用類型賦值給該變數時,則這個值的引用次數就是1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其所占的記憶體空間給收回來。這樣,垃圾收集器下次再運行時,它就會釋放那些引用次數為0的值所占的記憶體。
引用計數有個最大的問題: 迴圈引用。
比如對象A有一個屬性指向對象B,而對象B也有有一個屬性指向對象A,這樣相互引用.
function func() { let obj1 = {}; let obj2 = {}; obj1.a = obj2; // obj1 引用 obj2 obj2.a = obj1; // obj2 引用 obj1 }
在這個例子中,objA和objB通過各自的屬性相互引用;也就是說這兩個對象的引用次數都是2。在採用引用計數的策略中,由於函數執行之後,這兩個對象都離開了作用域,函數執行完成之後,objA和objB還將會繼續存在,因為他們的引用次數永遠不會是0。這樣的相互引用如果說很大量的存在就會導致大量的記憶體泄露。
解決:手動解除引用
obj1.a = null; obj2.a = null;
2、標記清除
這是javascript中最常用的垃圾回收方式。當變數進入執行環境是,就標記這個變數為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變數所占用的記憶體,因為只要執行流進入相應的環境,就可能會用到他們。當變數離開環境時,則將其標記為“離開環境”。
垃圾收集器在運行的時候會給存儲在記憶體中的所有變數都加上標記。然後,它會去掉環境中的變數以及被環境中的變數引用的標記。而在此之後再被加上標記的變數將被視為準備刪除的變數,原因是環境中的變數已經無法訪問到這些變數了。最後。垃圾收集器完成記憶體清除工作,銷毀那些帶標記的值,並回收他們所占用的記憶體空間。
標記清除也會遇到迴圈引用的問題。IE中有一部分對象並不是原生JavaScript對象。例如,其BOM和DOM中的對象就是使用C++以COM(Component Object Model,組件對象)對象的形式實現的,而COM對象的垃圾回收器就是採用的引用計數的策略。因此,即使IE的Javascript引擎使用標記清除的策略來實現的,但JavaScript訪問的COM對象依然是基於引用計數的策略的。說白了,只要IE中涉及COM對象,就會存在迴圈引用的問題。
解決:手工斷開js對象和DOM之間的鏈接。賦值為null。IE9把DOM和BOM轉換成真正的JS對象了,所以避免了這個問題。
5、避免垃圾回收
通過上面內容瞭解了,瀏覽器雖然可以自動化執行垃圾回收,但如果項目比較大代碼複雜,回收執行代價較大,某些情況甚至不能識別回收
1.數組array優化
將[]賦值給一個數組對象,是清空數組的捷徑(例如: arr = [];),但是需要註意的是,這種方式又創建了一個新的空對象,並且將原來的數組對象變成了一小片記憶體垃圾!實際上,將數組長度賦值為0(arr.length = 0)也能達到清空數組的目的,並且同時能實現數組重用,減少記憶體垃圾的產生。
2. 對象儘量復用
對象儘量復用,尤其是在迴圈等地方出現創建新對象,能復用就復用。不用的對象,儘可能設置為null,儘快被垃圾回收掉。
3.迴圈優化
在迴圈中的函數表達式,能復用最好放到迴圈外面。
6、避免記憶體泄漏
1.意外的全局變數
function foo(arg) { bar = "this is a hidden global variable"; }
bar沒被聲明,會變成一個全局變數,在頁面關閉之前不會被釋放。
另一種意外的全局變數可能由 this
創建:
function foo() { this.variable = "potential accidental global"; } // foo 調用自己,this 指向了全局對象(window) foo();
在 JavaScript 文件頭部加上 'use strict',可以避免此類錯誤發生。啟用嚴格模式解析 JavaScript ,避免意外的全局變數。
2.被遺忘的計時器或回調函數
var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { // 處理 node 和 someResource node.innerHTML = JSON.stringify(someResource)); } }, 1000);
這樣的代碼很常見,如果id為Node的元素從DOM中移除,該定時器仍會存在,同時,因為回調函數中包含對someResource的引用,定時器外面的someResource也不會被釋放。
3.閉包
function bindEvent(){ var obj=document.createElement('xxx') obj.onclick=function(){ // Even if it is a empty function } }
閉包可以維持函數內局部變數,使其得不到釋放。上例定義事件回調時,由於是函數內定義函數,並且內部函數--事件回調引用外部函數,形成了閉包
// 將事件處理函數定義在外面 function bindEvent() { var obj = document.createElement('xxx') obj.onclick = onclickHandler } // 或者在定義事件處理函數的外部函數中,刪除對dom的引用 function bindEvent() { var obj = document.createElement('xxx') obj.onclick = function() { // Even if it is a empty function } obj = null }
解決之道,將事件處理函數定義在外部,解除閉包,或者在定義事件處理函數的外部函數中,刪除對dom的引用。
4.沒有清理的DOM元素引用
有時,保存 DOM 節點內部數據結構很有用。假如你想快速更新表格的幾行內容,把每一行 DOM 存成字典(JSON 鍵值對)或者數組很有意義。此時,同樣的 DOM 元素存在兩個引用:一個在 DOM 樹中,另一個在字典中。將來你決定刪除這些行時,需要把兩個引用都清除。
var elements = { button: document.getElementById('button'), image: document.getElementById('image'), text: document.getElementById('text') }; function doStuff() { image.src = 'http://some.url/image'; button.click(); console.log(text.innerHTML); } function removeButton() { document.body.removeChild(document.getElementById('button')); // 此時,仍舊存在一個全局的 #button 的引用 // elements 字典。button 元素仍舊在記憶體中,不能被 GC 回收。 }
雖然我們用removeChild移除了button,但是還在elements對象里保存著#button的引用,換言之,DOM元素還在記憶體裡面。