瀏覽器的垃圾回收機制
一、垃圾回收概念
我們在寫 js 程式碼的時候,會頻繁地操作資料。在一些資料不被需要的時候,它就是垃圾資料,垃圾資料佔用的記憶體就應該被回收。
二、變數的生命週期
比如這麼一段程式碼:
let dog = new Object(); let dog.a = new Array(1);
- 當 JavaScript 執行這段程式碼的時候,會先在全域性作用域中新增一個
dog
屬性,並在堆中建立了一個空物件,將該物件的地址指向了dog
。 - 隨後又建立一個大小為 1 的陣列,並將屬性地址指向了
dog.a
。此時的記憶體佈局圖如下所示:
如果此時,我將另外一個物件賦給了a
屬性,程式碼如下所示:
dog.a = new Object()
此時的記憶體佈局圖:
a
的指向改變了, 此時堆中的陣列物件就成為了不被使用的資料,專業名詞叫不可達的資料。
這就是需要回收的垃圾資料。
三、垃圾回收演算法
可以將這個過程想象成從根溢位一個巨大的油漆桶,它從一個根節點出發將可到達的物件標記染色, 然後移除未標記的。
第一步:標記空間中可達值
V8 採用的是可達性 (reachability) 演算法來判斷堆中的物件應不應該被回收。
這個演算法的思路是這樣的:
- 從根節點(Root)出發,遍歷所有的物件。
- 可以遍歷到的物件,是可達的(reachable)。
- 沒有被遍歷到的物件,不可達
在瀏覽器環境下,根節點有很多,主要包括這幾種:
- 全域性變數
window
,位於每個iframe
中 - 文件
DOM
樹 - 存放在棧上的變數
- ...
這些根節點不是垃圾,不可能被回收。
第二步:回收「不可達」的值所佔據的記憶體
在所有的標記完成之後,統一清理記憶體中所有不可達的物件。
第三步,做記憶體整理
- 在頻繁回收物件後,記憶體中就會存在大量不連續空間,專業名詞叫記憶體碎片。
- 當記憶體中出現了大量的記憶體碎片,如果需要分配較大的連續記憶體時,就有可能出現記憶體不足的情況。
- 所以最後一步是整理記憶體碎片。(但這步其實是可選的,因為有的垃圾回收器不會產生記憶體碎片,比如副垃圾回收器)
四、分代收集
瀏覽器將資料分為兩種,一種是臨時物件,一種是長久物件。
臨時物件:
- 大部分物件在記憶體中存活的時間很短。
- 比如函式內部宣告的變數,或者塊級作用域中的變數。當函式或者程式碼塊執行結束時,作用域中定義的變數就會被銷燬。
- 這類物件很快就變得不可訪問,應該快點回收。
長久物件:
- 生命週期很長的物件,比如全域性的
window、DOM、Web API
等等。 - 這類物件可以慢點回收。
這兩種物件對應不同的回收策略,所以,V8 把堆分為新生代和老生代兩個區域, 新生代中存放臨時物件,老生代中存放持久物件。
並且讓副垃圾回收器負責新生代的垃圾回收、主垃圾回收器負責老生代的垃圾回收。
這樣就可以實現高效的垃圾回收啦。
主垃圾回收器
負責老生代的垃圾回收,有兩個特點:
- 物件佔用空間大。
- 物件存活時間長。
它使用標記-清除的演算法執行垃圾回收。
1.首先是標記
- 從一組根元素開始,遞迴遍歷這組根元素。
- 在這個遍歷過程中,能到達的元素稱為活動物件,沒有到達的元素就可以判斷為垃圾資料。
2.然後是垃圾清除
直接將標記為垃圾的資料清理掉。
3.多次標記-清除後,會產生大量不連續的記憶體碎片,需要進行記憶體整理。
副垃圾回收器
負責新生代的垃圾回收,通常只支援 1~8 M 的容量。
新生代被分為兩個區域:一半是物件區域,一半是空閒區域。
新加入的物件都被放入物件區域,等物件區域快滿的時候,會執行一次垃圾清理。
1.先給物件區域所有垃圾做標記。
2.標記完成後,存活的物件被複制到空閒區域,並且將他們有序的排列一遍。
副垃圾回收器沒有碎片整理。因為空閒區域裡此時是有序的,沒有碎片,也就不需要整理了。
3.複製完成後,物件區域會和空閒區域進行對調。將空閒區域中存活的物件放入物件區域裡。
這樣,就完成了垃圾回收。
因為副垃圾回收器操作比較頻繁,所以為了執行效率,一般新生區的空間會被設定得比較小。
一旦檢測到空間裝滿了,就執行垃圾回收。
- 一句話總結分代回收就是:將堆分為新生代與老生代,多回收新生代,少回收老生代。
- 這樣就減少了每次需遍歷的物件,從而減少每次垃圾回收的耗時。
五、增量收集
如果指令碼中有許多物件,引擎一次性遍歷整個物件,會造成一個長時間暫停。
所以引擎將垃圾收集工作分成更小的塊,每次處理一部分,多次處理。
這樣就解決了長時間停頓的問題。
六、閒時收集
垃圾收集器只會在 CPU 空閒時嘗試執行,以減少可能對程式碼執行的影響。
補充
瀏覽器中不同型別變數的記憶體都是何時釋放?
Javascritp 中型別:值型別,引用型別。
引用型別:
- 在沒有引用之後,通過 V8 自動回收。
值型別:
- 如果處於閉包的情況下,要等閉包沒有引用才會被 V8 回收。
- 非閉包的情況下,等待 V8 的新生代切換的時候回收。
哪些情況會導致記憶體洩露?如何避免?
記憶體洩露是指你用不到(訪問不到)的變數,依然佔居著記憶體空間,不能被再次利用起來。
以 Vue 為例,通常有這些情況:
- 監聽在
window/body
等事件沒有解綁 - 綁在
EventBus
的事件沒有解綁 Vuex
的$store
,watch
了之後沒有unwatch
- 使用第三方庫建立,沒有呼叫正確的銷燬函式
解決辦法:beforeDestroy
中及時銷燬
- 綁定了
DOM/BOM
物件中的事件addEventListener
,removeEventListener
。 - 觀察者模式
$on
,$off
處理。 - 如果元件中使用了定時器,應銷燬處理。
- 如果在
mounted/created
鉤子中使用了第三方庫初始化,對應的銷燬。 - 使用弱引用
weakMap
、weakSet
。
weakMap
weakSet
在 ES6 中為我們新增了兩個資料結構 WeakMap、WeakSet,就是為了解決記憶體洩漏的問題。
它的鍵名所引用的物件都是弱引用,就是垃圾回收機制遍歷的時候不考慮該引用。
只要所引用的物件的其他引用都被清除,垃圾回收機制就會釋放該物件所佔用的記憶體。
也就是說,一旦不再需要,WeakMap 裡面的鍵名物件和所對應的鍵值對會自動消失,不用手動刪除引用。