js垃圾回收機制


在js中創建一個變量時,會自動分配內存空間,當變量不再被使用時,垃圾回收機制會自動釋放相應的內存空間。

如何判斷一個變量不在被使用?方法有兩種:

一、引用計數法:

引用計數的判斷原理很簡單,就是看一份數據是否還有指向它的引用,若是沒有任何對象再指向它,那么垃圾回收器就會回收,其策略是跟蹤記錄每個變量值被使用的次數

  • 當聲明了一個變量並且將一個引用類型賦值給該變量時,這個值的引用次數就為 1

  • 如果同一個值又被賦給另一個變量,那么引用數加 1

  • 如果該變量的值被其他的值覆蓋了,則引用次數減 1

  • 當這個值的引用次數變為 0 的時候,說明沒有變量在使用,這個值沒法被訪問了,回收空間,垃圾回收器會在運行的時候清理掉引用次數為 0 的值占用的內存

引用計數存存在一個致命的缺陷,當對象間存在循環引用時,引用次數始終不會為0,因此垃圾回收器不會釋放它們。

function f() {
    var o1 = {};
    var o2 = {};
    o1.a = o2; // o1 引用 o2
    o2.a = o1; // o2 引用 o1
    return;
};
 
在 IE8 以及更早版本的 IE 中, BOM 和 DOM中的對象並不是原生的JS對象,而是使用C++以 COM對象的形式實現的,而 COM對象的垃圾收集機制采用的就是引用計數策略。因此,即使 IE 的 JavaScript引擎是使用標記清除策略來實現的,但JavaScript訪問的COM對象依然是基於引用計數策略的。換句話說,只要在IE8及以下版本中涉及 COM對象,就會存在循環引用的問題。下面這個簡單的例子,展示了使用 COM對象導致的循環引用問題;
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);

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM