我們知道,程序運(yùn)行中會有一些垃圾數(shù)據(jù)不再使用,需要及時釋放出去,如果我們沒有及時釋放,這就是內(nèi)存泄露。JavaScript 是一門具有自動垃圾收集機(jī)制的編程語言,由執(zhí)行環(huán)境負(fù)責(zé)在代碼執(zhí)行時管理內(nèi)存。JS 中的垃圾數(shù)據(jù)都是由垃圾回收(Garbage Collection,縮寫為 GC)器自動回收的,不需要手動釋放。JS 引擎中有一個后臺進(jìn)程稱為垃圾回收器,它監(jiān)視所有對象,觀察對象是否可被訪問,然后按照固定的時間間隔周期性的刪除掉那些不可訪問的對象即可。
常見的垃圾回收方法:1)引用計數(shù)方法;2)標(biāo)記清除方法。
1)引用計數(shù)
引用計數(shù)就是給一個占用物理空間的對象附加一個引用計數(shù)器,當(dāng)有其它對象引用這個對象時,這個對象的引用計數(shù)加一,反之解除時就減一,當(dāng)該對象引用計數(shù)為 0 時就會被回收。如果出現(xiàn)循環(huán)引用,則一直不會被回收,還是會造成內(nèi)存泄漏。
我們大部分人時刻都在寫著循環(huán)引用的代碼,看下面這個例子:

我們?yōu)橐粋€元素的點擊事件綁定了一個匿名函數(shù),我們通過event參數(shù)是可以拿到相應(yīng)元素el的信息的。el有一個屬性onclick引用了一個函數(shù)(其實也是個對象),函數(shù)里面的參數(shù)又引用了el,這樣el的引用次數(shù)一直是2,即使當(dāng)前這個頁面關(guān)閉了,也無法進(jìn)行垃圾回收。
解決辦法:及時銷毀綁定的事件、使用弱引用?weakMap、weakSet。
2)標(biāo)記清除
V8 中主垃圾回收器就采用標(biāo)記清除法進(jìn)行垃圾回收。主要流程如下:
標(biāo)記:遍歷調(diào)用棧,看老生代區(qū)域堆中的對象是否被引用,被引用的對象標(biāo)記為活動對象,沒有被引用的對象(待清理)標(biāo)記為垃圾數(shù)據(jù)。
清除:將所有垃圾數(shù)據(jù)清理掉。

垃圾回收算法
垃圾回收的實現(xiàn)簡單分為以下三個步驟:
1)可訪問性
從 GC Roots 對象出發(fā),遍歷 GC Root 中的所有對象:
a、可訪問對象:通過 GC Root 遍歷到的對象,我們就認(rèn)為該對象是可訪問的(reachable),那么必須保證這些對象應(yīng)該在內(nèi)存中保留。
b、不可訪問對象:通過 GC Roots 沒有遍歷到的對象,則是不可訪問的(unreachable),并會對其做上標(biāo)記,那么這些不可訪問的對象就可能被回收。
GC Root 有很多,通常包括了以下幾種 (但是不止于這幾種):全局的 window 對象(位于每個 iframe 中);文檔 DOM 樹,可以通過遍歷文檔到達(dá)的所有原生 DOM 節(jié)點組成;存放棧上變量。
2)回收不可訪問對象所占據(jù)的內(nèi)存
在所有的標(biāo)記完成之后,統(tǒng)一清理內(nèi)存中所有被標(biāo)記為可回收的對象。
3)內(nèi)存整理
頻繁回收對象后,內(nèi)存中就會存在大量不連續(xù)空間,稱為內(nèi)存碎片。當(dāng)出現(xiàn)了大量的內(nèi)存碎片之后,如果需要分配較大的連續(xù)內(nèi)存時,就會出現(xiàn)內(nèi)存不足的情況,所以最后一步需要整理這些內(nèi)存碎片。
在我們的實際開發(fā)過程中,如果我們想要讓垃圾回收器回收某一對象,就將對象的引用直接設(shè)置為?null。但如果一個對象被多次引用時,例如作為另一對象的鍵、值或子元素時,將該對象引用設(shè)置為?null?時,該對象是不會被回收的,依然存在。

如果想讓 a 置為?null?時,該對象被回收,該怎么做?ES6 考慮到了這一點,推出了WeakMap?。它對于值的引用都是不計入垃圾回收機(jī)制的,所以名字里面才會有一個"Weak",表示這是弱引用(對對象的弱引用是指當(dāng)該對象應(yīng)該被GC回收時不會阻止GC的回收行為)。


從上面的例子我們可以看出,只要外部的引用消失,WeakMap 內(nèi)部的引用,就會自動被垃圾回收清除。
WeakMap 對象是一組鍵值對的集合,其中的鍵是弱引用對象,而值可以是任意。
注意,WeakMap 弱引用的只是鍵名,而不是鍵值。鍵值依然是正常引用。
WeakMap 中,每個鍵對自己所引用對象的引用都是弱引用,在沒有其他引用和該鍵引用同一對象,這個對象將會被垃圾回收(相應(yīng)的key則變成無效的),所以,WeakMap 的 key 是不可枚舉的。
除了?WeakMap?還有?WeakSet?都是弱引用,可以被垃圾回收機(jī)制回收,可以用來保存DOM節(jié)點,不容易造成內(nèi)存泄漏。另外還有 ES12 的?WeakRef。
WeakMap vs Map(詳情見 http://www.itdecent.cn/p/114ec4a13e4e)
JavaScript 引擎 V8 - 垃圾回收
目前 V8 采用了兩個垃圾回收器,主垃圾回收器和副垃圾回收器。在 V8 中,會把堆分為新生代(新生代通常只支持 1~8M 的容量)和老生代(容量大)兩個區(qū)域,新生代中存放的是生存時間短的對象,老生代中存放生存時間久的對象。分別對新老生代采用不同的垃圾回收算法來提高效率,對象最開始都會先被分配到新生代(如果新生代內(nèi)存空間不夠,直接分配到老生代),新生代中的對象會在滿足某些條件后,被移動到老生代,這個過程叫晉升(當(dāng)一個對象經(jīng)過多次復(fù)制仍然存活時,它就會被認(rèn)為是生命周期較長的對象。這種較長生命周期的對象隨后會被移動到老生代中,采用新的算法進(jìn)行管理)。
對象晉升的條件主要有兩個:
a、對象從對象區(qū)復(fù)制到空閑區(qū)時,會檢查它的內(nèi)存地址來判斷這個對象是否已經(jīng)經(jīng)歷過一次Scavenge回收。如果已經(jīng)經(jīng)歷過了,會將該對象從新生空間移動到老生代空間中,如果沒有,則復(fù)制到空閑區(qū)。總結(jié)來說,如果一個對象是第二次經(jīng)歷從對象區(qū)復(fù)制到空閑區(qū),那么這個對象會被移動到老生代中。
b、當(dāng)要從對象區(qū)復(fù)制一個對象到空閑區(qū)時,如果空閑區(qū)已經(jīng)使用了超過25%,則這個對象直接晉升到老生區(qū)中。設(shè)置25%這個閾值的原因是當(dāng)這次Scavenge回收完成后,這個空閑區(qū)會變?yōu)閷ο髤^(qū),接下來的內(nèi)存分配將在這個空間中進(jìn)行。如果占比過高,會影響后續(xù)的內(nèi)存分配。
1)副垃圾回收器
負(fù)責(zé)新生代的垃圾回收,大多數(shù)小的對象都會被分配到新生代,垃圾回收比較頻繁。新生代中的垃圾數(shù)據(jù)用 Scavenge 算法來處理。Cheney算法將內(nèi)存一分為二,叫做semispace,分為兩個區(qū)域:對象區(qū)域 ,空閑區(qū)域。

垃圾回收過程:新加入的對象都會存放到對象區(qū)域,當(dāng)對象區(qū)域快被寫滿時,就需要執(zhí)行一次垃圾清理操作。
a、垃圾標(biāo)記和清理:首先要對對象區(qū)域中的垃圾做標(biāo)記;標(biāo)記完成之后,就進(jìn)入垃圾清理階段。副垃圾回收器會把這些我們?nèi)匀辉谟玫膶ο髲?fù)制到空閑區(qū)域中,同時它還會把這些對象有序地排列起來,在復(fù)制過程,相當(dāng)于完成了內(nèi)存整理操作,復(fù)制后空閑區(qū)域就沒有內(nèi)存碎片了。

b、角色翻轉(zhuǎn):完成復(fù)制后,進(jìn)行角色翻轉(zhuǎn)。把原來的對象區(qū)變成空閑區(qū),把原來的空閑區(qū)變成對象區(qū)。是為了讓活躍對象始終保持在一塊semispace中,另一塊semispace始終保持空閑的狀態(tài)。

2)主垃圾回收器
負(fù)責(zé)老生代中的垃圾回收,大多數(shù)占用空間大、存活時間長的對象都會被分配到老生代里。老生代中的垃圾數(shù)據(jù)用標(biāo)記 - 清除算法進(jìn)行垃圾回收,因為老生代中的對象通常比較大,復(fù)制大對象非常耗時,會導(dǎo)致回收執(zhí)行效率不高,所以采用標(biāo)記清除法。
垃圾回收過程:
a、標(biāo)記:標(biāo)記階段就是從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程中,能到達(dá)的元素稱為活動對象,沒有到達(dá)的元素就可以判斷為垃圾數(shù)據(jù)。
b、清除:它和副垃圾回收器的垃圾清除過程完全不同,主垃圾回收器會直接將標(biāo)記為垃圾的數(shù)據(jù)清理掉。

c、整理:清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,過多的碎片會導(dǎo)致大對象無法分配到足夠的連續(xù)內(nèi)存,于是需要引進(jìn)另一種算法:標(biāo)記 - 整理,整理這些內(nèi)存碎片。

由于 JavaScript 是運(yùn)行在主線程之上的,在垃圾回收時會阻塞 JavaScript 腳本的執(zhí)行,會造成頁面卡頓等問題。
為了解決上述問題,V8 團(tuán)隊推出了并行、并發(fā)和增量等垃圾回收技術(shù)。
a、將一個完整的垃圾回收的任務(wù)拆分成多個小的任務(wù),解決單個垃圾回收時間長的問題。
b、將標(biāo)記對象、移動對象等任務(wù)轉(zhuǎn)移到后臺線程進(jìn)行,減少主阻塞線程的時間。