javascript 垃圾回收機制

李赫feixuan發表於2018-08-06

---------------------這是學習筆記---------------------

隨著前端業務需求的不斷增多,相比以前,我們會佔用更多的記憶體。但是記憶體並不是無限的,而對於那些我們不再需要的變數、物件該怎麼處理呢?難道一個一個去手動釋放麼?其實並不需要,Javascript 具有自動垃圾回收機制,會定期對那些我們不再使用的變數、物件所佔用的記憶體進行釋放

Javascript 的垃圾回收機制

Javascript 會找出不再使用的變數,不再使用意味著這個變數生命週期的結束。Javascript 中存在兩種變數——全域性變數和區域性變數,全部變數的宣告週期會一直持續,直到頁面解除安裝

而區域性變數宣告在函式中,它的宣告週期從執行函式開始,直到函式執行結束。在這個過程中,區域性變數會在堆或棧上被分配相應的空間以儲存它們的值,函式執行結束,這些區域性變數也不再被使用,它們所佔用的空間也就被釋放

但是有一種情況的區域性變數不會隨著函式的結束而被回收,那就是區域性變數被函式外部的變數所使用,其中一種情況就是閉包,因為在函式執行結束後,函式外部的變數依然指向函式內的區域性變數,此時的區域性變數依然在被使用,所以也就不能夠被回收

function func1 () {
      const obj = {}
}

function func2 () {
      const obj = {}
      return obj
}

const a = func1()
const b = func2()
複製程式碼

上面這個例子中,func1 執行時為 obj 分配了一塊記憶體,但是隨著函式執行結束,obj佔用的空間也就被釋放了;而 func2 執行時,也為 obj 分配了記憶體,但是由於 obj 最終被返回賦值給了 b 導致其依然被使用,所以 func2 中的 obj 佔用的記憶體不會被釋放

垃圾回收的兩種實現方式

垃圾回收有兩種實現方式,分別是標記清除引用計數

標記清楚

當變數進入執行環境時標記為“進入環境”,當變數離開執行環境時則標記為“離開環境”,被標記為“進入環境”的變數是不能被回收的,因為它們正在被使用,而標記為“離開環境”的變數則可以被回收

function func3 () {
      const a = 1
      const b = 2
      // 函式執行時,a b 分別被標記 進入環境
}

func3() // 函式執行結束,a b 被標記 離開環境,被回收
複製程式碼

引用計數

統計引用型別變數宣告後被引用的次數,當次數為 0 時,該變數將被回收

function func4 () {
      const c = {} // 引用型別變數 c的引用計數為 0
      let d = c // c 被 d 引用 c的引用計數為 1
      let e = c // c 被 e 引用 c的引用計數為 2
      d = {} // d 不再引用c c的引用計數減為 1
      e = null // e 不再引用 c c的引用計數減為 0 將被回收
}
複製程式碼

但是引用計數的方式,有一個相對明顯的缺點——迴圈引用

function func5 () {
      let f = {}
      let g = {}
      f.prop = g
      g.prop = f
      // 由於 f 和 g 互相引用,計數永遠不可能為 0
}
複製程式碼

像上面這種情況就需要手動將變數的記憶體釋放

f.prop = null
g.prop = null
複製程式碼

在現代瀏覽器中,Javascript 使用的方式是標記清楚,所以我們無需擔心迴圈引用的問題

什麼是記憶體洩露?

本質上講, 記憶體洩露就是不再被需要的記憶體, 由於某種原因, 無法被釋放.

常見的記憶體洩露案例

1)全域性變數照成記憶體洩露

             function fn() {
   		name = "你我貸"
             }
   	     console.log(name)複製程式碼

javascript 垃圾回收機制

在 JS 中處理未被宣告的變數, 上述範例中的會把 name , 定義到全域性物件中, 在瀏覽器中就是 window 上. 在頁面中的全域性變數, 只有當頁面被關閉後才會被銷燬. 所以這種寫法就會造成記憶體洩露, 當然在這個例子中洩露的只是一個簡單的字串, 但是在實際的程式碼中, 往往情況會更加糟糕.

另外一種意外建立全域性變數的情況.

                function fn() {
   			this.name = "你我貸"
   		}
   		console.log(name)複製程式碼

javascript 垃圾回收機制

在這種情況下this被指向了全域性變數 window, 意外的建立了全域性變數. 我們談到了一些意外情況下定義的全域性變數, 程式碼中也有一些我們明確定義的全域性變數. 如果使用這些全域性變數用來暫存大量的資料, 記得在使用後, 對其重新賦值為 null.

2)未銷燬的定時器和回撥函式照成記憶體洩露

        function fn() {
    		return 2
    	}
    	var oTxt = fn();
   	setInterval(function() {
	    var oHtml = document.getElementById("test")
	    if(oHtml) {
	        oHtml.innerHTML = oTxt;
	    }
	}, 1000); // 每 1 秒呼叫一次複製程式碼

如果後續 oHtml 元素被移除, 整個定時器實際上沒有任何作用. 但如果你沒有回收定時器, 整個定時器依然有效, 不但定時器無法被記憶體回收, 定時器函式中的依賴也無法回收. 在這個案例中的 fn 也無法被回收.

3 ) 閉包照成記憶體洩露

在 JS 開發中, 我們會經常用到閉包, 一個內部函式, 有權訪問包含其的外部函式中的變數. 下面這種情況下, 閉包也會造成記憶體洩露.


3)DOM 引用照成記憶體洩露

很多時候, 我們對 Dom 的操作, 會把 Dom 的引用儲存在一個陣列或者 Map 中.

        var elements = {
    		txt: document.getElementById("test")
    	}
    	function fn() {
    		elements.txt.innerHTML = "1111"
    	}
    	function removeTxt() {
    		document.body.removeChild(document.getElementById('test'));
    	}
    	fn();
    	removeTxt()
    	console.log(elements.txt)複製程式碼

javascript 垃圾回收機制

上述案例中, 即使我們對於 test 元素進行了移除, 但是仍然有對 test 元素的引用, 依然無法對齊進行記憶體回收. 另外需要注意的一個點是, 對於一個 Dom 樹的葉子節點的引用. 舉個例子: 如果我們引用了一個表格中的 td 元素, 一旦在 Dom 中刪除了整個表格, 我們直觀的覺得記憶體回收應該回收除了被引用的 td 外的其他元素. 但是事實上, 這個 td 元素是整個表格的一個子元素, 並保留對於其父元素的引用. 這就會導致對於整個表格, 都無法進行記憶體回收. 所以我們要小心處理對於 Dom 元素的引用.

相關文章