在js中創建一個變量時,會自動分配內存空間,當變量不再被使用時,垃圾回收機制會自動釋放相應的內存空間。
如何判斷一個變量不在被使用?方法有兩種:
一、引用計數法:
引用計數的判斷原理很簡單,就是看一份數據是否還有指向它的引用,若是沒有任何對象再指向它,那么垃圾回收器就會回收,其策略是跟蹤記錄每個變量值被使用的次數
-
當聲明了一個變量並且將一個引用類型賦值給該變量時,這個值的引用次數就為 1
-
如果同一個值又被賦給另一個變量,那么引用數加 1
-
如果該變量的值被其他的值覆蓋了,則引用次數減 1
-
當這個值的引用次數變為 0 的時候,說明沒有變量在使用,這個值沒法被訪問了,回收空間,垃圾回收器會在運行的時候清理掉引用次數為 0 的值占用的內存
引用計數存存在一個致命的缺陷,當對象間存在循環引用時,引用次數始終不會為0,因此垃圾回收器不會釋放它們。
function f() { var o1 = {}; var o2 = {}; o1.a = o2; // o1 引用 o2 o2.a = o1; // o2 引用 o1 return; };
var element = document.getElementById("some_element"); var myObject = new Object{); myObject. element = element; element.someObject = myObject;
這個例子在一個DOM元素(element)與一個原生 JavaScript對象(myobject)之間創建了循環引用。而想要解決循環引用,需要將引用地址置為 null
來切斷變量與之前引用值的關系,當垃圾收集器下次運行時,就會刪除這些值並回收它們占用的內存。
myObject.element = null; element,SomeObject = null;
二、標記清除法:
標記清除(Mark-Sweep),目前在 JavaScript引擎
里這種算法是最常用的,到目前為止的大多數瀏覽器的 JavaScript引擎
都在采用標記清除算法。
此算法可以分為兩個階段,一個是標記階段(mark),一個是清除階段(sweep)。
標記階段,垃圾回收器會從根對象開始遍歷(在js中,通常認定全局對象window做為根)。每一個可以從根對象訪問到的對象都會被添加一個標識,於是這個對象就被標識為可到達對象。
清除階段,垃圾回收器會對堆內存從頭到尾進行線性遍歷,如果發現有對象沒有被標識為可到達對象,那么就將此對象占用的內存回收,並且將原來標記為可到達對象的標識清除,以便進行下一次垃圾回收操作。
在標記階段,從根對象1可以訪問到B,從B又可以訪問到E,那么B和E都是可到達對象,同樣的道理,F、G、J和K都是可到達對象。
在回收階段,所有未標記為可到達的對象都會被垃圾回收器回收。
標記清除法會導致內存碎片化。由於空閑內存塊是不連續的,容易出現很多空閑內存塊,假設我們新建對象分配內存時需要大小為 size
,由於空閑內存是間斷的、不連續的,則需要對空閑內存列表進行一次單向遍歷找出大於等於 size
的塊才能為其分配(如下圖)
那如何找到合適的塊呢?我們可以采取下面三種分配策略
-
First-fit
,找到大於等於size
的塊立即返回 -
Best-fit
,遍歷整個空閑列表,返回大於等於size
的最小分塊 -
Worst-fit
,遍歷整個空閑列表,找到最大的分塊,然后切成兩部分,一部分size
大小,並將該部分返回
這三種策略里面 Worst-fit
的空間利用率看起來是最合理,但實際上切分之后會造成更多的小塊,形成內存碎片,所以不推薦使用,對於 First-fit
和 Best-fit
來說,考慮到分配的速度和效率 First-fit
是更為明智的選擇,但即便是使用 First-fit
策略,其操作仍是一個 O(n)
的操作,最壞情況是每次都要遍歷到最后,同時因為碎片化,大對象的分配效率會更慢
三、v8引擎的垃圾回收機制
Chrome 瀏覽器所使用的 V8 引擎采用的分代回收策略,該策略通過區分「臨時」與「持久」對象;多回收「臨時對象區」(young generation),少回收「持久對象區」(tenured generation),減少每次需遍歷的對象,從而減少每次GC的耗時。
「臨時」與「持久」對象也被叫做作「新生代」與「老生代」對象。
1. 新生代的特點:
- 通常把小的對象分配到新生代
- 新生代的垃圾回收比較頻繁
- 通常存儲容量在1~8M
2. 新生代-Scavenge算法
該算法將新生代分為兩部分,一部分叫做from(對象區域),另一部分叫做to(空閑區域),新加入的對象首先存放在from區域;
from區域寫滿的時候,對from區域開始進行垃圾回收。首先對from區域的垃圾進行標記(紅色代表標記為垃圾);
將存活的對象復制到to區域中,並且有序地排列起來,復制后的to區域就沒有內存碎片了;
清空from區域;
from區域和to區域進行反轉,也就是原來的from區域變為to區域,原來的to區域變成from區域。
Scavenge算法在時間效率上有着優異的表現,缺點是只能使用堆內存中的一半,如果存儲容量過大,就會導致每次清理的時間過長,效率低,因此經過兩次垃圾回收之后依然存活的對象會晉升為老生代對象,另外還有一種情況,如果復制一個對象到空閑區時,空閑區空間占用超過了 25%,那么這個對象會被直接晉升到老生代空間中,設置為 25% 的比例的原因是,當完成 Scavenge
回收后,空閑區將翻轉成對象區域,繼續進行對象內存的分配,若占比過大,將會影響后續內存分配。
3. 老生代的特點:
- 對象占用空間大
- 對象存活時間長
4. 老生代-標記整理法
- 標記:和標記 - 清除的標記過程一樣,從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程中,能到達的元素標記為活動對象;
- 整理:讓所有存活的對象都向內存的一端移動
5. 何時執行垃圾回收?
由於 JavaScript 是運行在主線程之上的,一旦執行垃圾回收算法,都需要將正在執行的 JavaScript 腳本暫停下來,待垃圾回收完畢后再恢復腳本執行。我們把這種行為叫做全停頓(Stop-The-World)。
為了降低老生代的垃圾回收而造成的卡頓,V8 將標記過程分為一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,我們把這個算法稱為增量標記(Incremental Marking)算法
四、哪些情況容易引起內存泄漏
1. 全局變量
全局變量等同於在window上添加屬性,因此在函數執行完畢,依舊能夠訪問到它,因此不能夠被回收。
2. 閉包
3. 被遺忘的定時器或事件回調函數
當dom元素被移除時,因為是周期定時器的緣故,定時器回調函數始終沒法被回收,這也致使了定時器會一直對數據serverData保持引用,好的作法是在不須要時中止定時器
var serverData = loadData(); setInterval(function () { var dom = document.getElementById('renderer'); if (dom) { dom.innerHTML = JSON.stringify(serverData); } }, 3000);
另外在使用事件監聽時,若是再也不須要監聽記得移除監聽事件
var element = document.getElementById('button'); function onclick(event) { element.innerHTML = 'text'; }; element.addEventListener('click', onclick); // 移除監聽 element.removeEventListener('click', onclick);