4 個問題圖解瀏覽器垃圾回收的過程。

Java鬥帝之路發表於2020-10-07

瀏覽器垃圾回收一直是前端面試常考的部分,我一直不太理解。最近深入學習了一下,爭取一篇文章說清楚。

我們首先帶著這 4 個問題,來了解瀏覽器垃圾回收的過程,後面會逐一解答:

  1. 瀏覽器怎麼進行垃圾回收?
  2. 瀏覽器中不同型別變數的記憶體都是何時釋放?
  3. 哪些情況會導致記憶體洩露?如何避免?
  4. weakMap weakSet 和 Map Set 有什麼區別?

ok, let's go!

什麼是垃圾資料?

生活中你買了一瓶可樂,喝完之後可樂瓶就變成了垃圾,應該被回收處理。

同樣地,我們在寫 js 程式碼的時候,會頻繁地運算元據。

在一些資料不被需要的時候,它就是垃圾資料,垃圾資料佔用的記憶體就應該被回收。

變數的生命週期

比如這麼一段程式碼:

let dog = new Object()let dog.a = new Array(1)

當 JavaScript 執行這段程式碼的時候,

會先在全域性作用域中新增一個dog 屬性,並在堆中建立了一個空物件,將該物件的地址指向了 dog。

隨後又建立一個大小為 1 的陣列,並將屬性地址指向了 dog.a。此時的記憶體佈局圖如下所示:

4 個問題圖解瀏覽器垃圾回收的過程。

如果此時,我將另外一個物件賦給了 a 屬性,程式碼如下所示:

dog.a = new Object()複製程式碼

此時的記憶體佈局圖:

4 個問題圖解瀏覽器垃圾回收的過程。

a 的指向改變了, 此時堆中的陣列物件就成為了不被使用的資料,專業名詞叫「不可達」的資料。

這就是需要回收的垃圾資料。

垃圾回收演算法

可以將這個過程想象成從根溢位一個巨大的油漆桶,它從一個根節點出發將可到達的物件標記染色, 然後移除未標記的。

第一步:標記空間中「可達」值。

V8 採用的是可達性 (reachability) 演算法來判斷堆中的物件應不應該被回收。

這個演算法的思路是這樣的:

  • 從根節點(Root)出發,遍歷所有的物件。
  • 可以遍歷到的物件,是可達的(reachable)。
  • 沒有被遍歷到的物件,不可達的(unreachable)。

在瀏覽器環境下,根節點有很多,主要包括這幾種:

  • 全域性變數 window,位於每個 iframe 中
  • 文件 DOM 樹
  • 存放在棧上的變數
  • ...

這些根節點不是垃圾,不可能被回收。

第二步:回收「不可達」的值所佔據的記憶體。

在所有的標記完成之後,統一清理記憶體中所有不可達的物件。

第三步,做記憶體整理。

  • 在頻繁回收物件後,記憶體中就會存在大量不連續空間,專業名詞叫「記憶體碎片」。
  • 當記憶體中出現了大量的記憶體碎片,如果需要分配較大的連續記憶體時,就有可能出現記憶體不足的情況。
  • 所以最後一步是整理記憶體碎片。(但這步其實是可選的,因為有的垃圾回收器不會產生記憶體碎片,比如接下來我們要介紹的副垃圾回收器。)

什麼時候垃圾回收?

瀏覽器進行垃圾回收的時候,會暫停 JavaScript 指令碼,等垃圾回收完畢再繼續執行。

對於普通應用這樣沒什麼問題,但對於 JS 遊戲、動畫對連貫性要求比較高的應用,如果暫停時間很長就會造成頁面卡頓。

這就是我們接下來談的關於垃圾回收的問題:什麼時候進行垃圾回收,可以避免長時間暫停。

分代收集

瀏覽器將資料分為兩種,一種是「臨時」物件,一種是「長久」物件。

  • 臨時物件:
    • 大部分物件在記憶體中存活的時間很短。
    • 比如函式內部宣告的變數,或者塊級作用域中的變數。當函式或者程式碼塊執行結束時,作用域中定義的變數就會被銷燬。
    • 這類物件很快就變得不可訪問,應該快點回收。
  • 長久物件:
    • 生命週期很長的物件,比如全域性的 window、DOM、Web API 等等。
    • 這類物件可以慢點回收。

這兩種物件對應不同的回收策略,所以,V8 把堆分為新生代和老生代兩個區域, 新生代中存放臨時物件,老生代中存放持久物件。

並且讓副垃圾回收器、主垃圾回收器,分別負責新生代、老生代的垃圾回收。

這樣就可以實現高效的垃圾回收啦。

一般來說,面試回答到這就夠了。如果想和麵試官深入交流,可以繼續聊聊兩個垃圾回收器。

主垃圾回收器

負責老生代的垃圾回收,有兩個特點:

  1. 物件佔用空間大。
  2. 物件存活時間長。

它使用「標記-清除」的演算法執行垃圾回收。

  1. 首先是標記。
  2. 從一組根元素開始,遞迴遍歷這組根元素。
  3. 在這個遍歷過程中,能到達的元素稱為活動物件,沒有到達的元素就可以判斷為垃圾資料。
  4. 然後是垃圾清除。直接將標記為垃圾的資料清理掉。
  5. 多次標記-清除後,會產生大量不連續的記憶體碎片,需要進行記憶體整理。

副垃圾回收器

負責新生代的垃圾回收,通常只支援 1~8 M 的容量。

新生代被分為兩個區域:一般是物件區域,一半是空閒區域。

4 個問題圖解瀏覽器垃圾回收的過程。

新加入的物件都被放入物件區域,等物件區域快滿的時候,會執行一次垃圾清理。

  1. 先給物件區域所有垃圾做標記。
  2. 標記完成後,存活的物件被複制到空閒區域,並且將他們有序的排列一遍。這就回到我們前面留下的問題 -- 副垃圾回收器沒有碎片整理。因為空閒區域裡此時是有序的,沒有碎片,也就不需要整理了。
  3. 複製完成後,物件區域會和空閒區域進行對調。將空閒區域中存活的物件放入物件區域裡。這樣,就完成了垃圾回收。

因為副垃圾回收器操作比較頻繁,所以為了執行效率,一般新生區的空間會被設定得比較小。

一旦檢測到空間裝滿了,就執行垃圾回收。

分代收集

一句話總結分代回收就是:將堆分為新生代與老生代,多回收新生代,少回收老生代。

這樣就減少了每次需遍歷的物件,從而減少每次垃圾回收的耗時。

4 個問題圖解瀏覽器垃圾回收的過程。

增量收集

如果指令碼中有許多物件,引擎一次性遍歷整個物件,會造成一個長時間暫停。

所以引擎將垃圾收集工作分成更小的塊,每次處理一部分,多次處理。

這樣就解決了長時間停頓的問題。

4 個問題圖解瀏覽器垃圾回收的過程。

閒時收集

垃圾收集器只會在 CPU 空閒時嘗試執行,以減少可能對程式碼執行的影響。

面試題1:瀏覽器怎麼進行垃圾回收?

從三個點來回答什麼是垃圾、如何撿垃圾、什麼時候撿垃圾。

  1. 什麼是垃圾
  2. 不再需要,即為垃圾
  3. 全域性變數隨時可能用到,所以一定不是垃圾
  4. 如何撿垃圾(遍歷演算法)
  5. 標記空間中「可達」值。- 從根節點(Root)出發,遍歷所有的物件。
    - 可以遍歷到的物件,是可達的(reachable)。
    - 沒有被遍歷到的物件,不可達的(unreachable)
  6. 回收「不可達」的值所佔據的記憶體。
  7. 做記憶體整理。
  8. 什麼時候撿垃圾
  9. 前端有其特殊性,垃圾回收的時候會造成頁面卡頓。
  10. 分代收集、增量收集、閒時收集。

面試題2:瀏覽器中不同型別變數的記憶體都是何時釋放?

Javascritp 中型別:值型別,引用型別。

  • 引用型別
    • 在沒有引用之後,通過 V8 自動回收。
  • 值型別
    • 如果處於閉包的情況下,要等閉包沒有引用才會被 V8 回收。
    • 非閉包的情況下,等待 V8 的新生代切換的時候回收。

面試題3:哪些情況會導致記憶體洩露?如何避免?

記憶體洩露是指你「用不到」(訪問不到)的變數,依然佔居著記憶體空間,不能被再次利用起來。

以 Vue 為例,通常有這些情況:

  • 監聽在 window/body 等事件沒有解綁
  • 綁在 EventBus 的事件沒有解綁
  • Vuex 的 $store,watch 了之後沒有 unwatch
  • 使用第三方庫建立,沒有呼叫正確的銷燬函式

解決辦法:beforeDestroy 中及時銷燬

  • 繫結了 DOM/BOM 物件中的事件 addEventListener ,removeEventListener。
  • 觀察者模式 $on,$off處理。
  • 如果元件中使用了定時器,應銷燬處理。
  • 如果在 mounted/created 鉤子中使用了第三方庫初始化,對應的銷燬。
  • 使用弱引用 weakMap、weakSet。

閉包會導致記憶體洩露嗎?

順便說一個我在瞭解垃圾回收之前對閉包的誤解。

閉包會導致記憶體洩露嗎?正確的答案是不會。

記憶體洩露是指你「用不到」(訪問不到)的變數,依然佔居著記憶體空間,不能被再次利用起來。

閉包裡面的變數就是我們需要的變數,不能說是記憶體洩露。

這個誤解是如何來的?因為 IE。IE 有 bug,IE 在我們使用完閉包之後,依然回收不了閉包裡面引用的變數。這是 IE 的問題,不是閉包的問題。參考這篇文章

面試題4:weakMap weakSet 和 Map Set 有什麼區別?

在 ES6 中為我們新增了兩個資料結構 WeakMap、WeakSet,就是為了解決記憶體洩漏的問題。

它的鍵名所引用的物件都是弱引用,就是垃圾回收機制遍歷的時候不考慮該引用。

只要所引用的物件的其他引用都被清除,垃圾回收機制就會釋放該物件所佔用的記憶體。

也就是說,一旦不再需要,WeakMap 裡面的鍵名物件和所對應的鍵值對會自動消失,不用手動刪除引用。

更全面的介紹可以看這裡:第 4 題:介紹下 Set、Map、WeakSet 和 WeakMap 的區別

總結

現在我們簡單瞭解了瀏覽器的垃圾回收機制,還記得最初的 4 個問題嗎?

  1. 瀏覽器怎麼進行垃圾回收?

答題思路:什麼是垃圾、怎麼收垃圾、什麼時候收垃圾。

  1. 瀏覽器中不同型別變數的記憶體都是何時釋放?

答題思路:分為值型別、引用型別。

  1. 哪些情況會導致記憶體洩露?如何避免?

答題思路:記憶體洩露是指你「用不到」(訪問不到)的變數,依然佔居著記憶體空間,不能被再次利用起來。

  1. weakMap weakSet 和 Map Set 有什麼區別?

答題思路:WeakMap、WeakSet 弱引用,解決了記憶體洩露問題。

看完三件事❤️

如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。

  2. 關注公眾號 『 Java鬥帝 』,不定期分享原創知識。

  3. 同時可以期待後續文章ing?

 

相關文章