瀏覽器的垃圾回收機制


一、垃圾回收概念

  我們在寫 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)。
  • 沒有被遍歷到的對象,不可達的(unreachable)。

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

  • 全局變量 window,位於每個 iframe 中
  • 文檔 DOM 樹
  • 存放在棧上的變量
  • ...

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

第二步:回收「不可達」的值所占據的內存

  在所有的標記完成之后,統一清理內存中所有不可達的對象。

第三步,做內存整理

  • 在頻繁回收對象后,內存中就會存在大量不連續空間,專業名詞叫內存碎片
  • 當內存中出現了大量的內存碎片,如果需要分配較大的連續內存時,就有可能出現內存不足的情況。
  • 所以最后一步是整理內存碎片。(但這步其實是可選的,因為有的垃圾回收器不會產生內存碎片,比如副垃圾回收器)

四、分代收集

  瀏覽器將數據分為兩種,一種是臨時對象,一種是長久對象。

  臨時對象:

  • 大部分對象在內存中存活的時間很短。
  • 比如函數內部聲明的變量,或者塊級作用域中的變量。當函數或者代碼塊執行結束時,作用域中定義的變量就會被銷毀。
  • 這類對象很快就變得不可訪問,應該快點回收。

  長久對象:

  • 生命周期很長的對象,比如全局的 window、DOM、Web API 等等。
  • 這類對象可以慢點回收。

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

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

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

主垃圾回收器

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

  1. 對象占用空間大。
  2. 對象存活時間長。

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

  1.首先是標記

  • 從一組根元素開始,遞歸遍歷這組根元素。
  • 在這個遍歷過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾數據。

  2.然后是垃圾清除

  直接將標記為垃圾的數據清理掉。

  3.多次標記-清除后,會產生大量不連續的內存碎片,需要進行內存整理。

副垃圾回收器

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

  新生代被分為兩個區域:一半是對象區域,一半是空閑區域。

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

  1.先給對象區域所有垃圾做標記。

  2.標記完成后,存活的對象被復制到空閑區域,並且將他們有序的排列一遍。

  副垃圾回收器沒有碎片整理。因為空閑區域里此時是有序的,沒有碎片,也就不需要整理了。

  3.復制完成后,對象區域會和空閑區域進行對調。將空閑區域中存活的對象放入對象區域里。

  這樣,就完成了垃圾回收。

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

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

  • 一句話總結分代回收就是:將堆分為新生代與老生代,多回收新生代,少回收老生代。
  • 這樣就減少了每次需遍歷的對象,從而減少每次垃圾回收的耗時。

 五、增量收集

  如果腳本中有許多對象,引擎一次性遍歷整個對象,會造成一個長時間暫停。

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

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

六、閑時收集

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

補充

瀏覽器中不同類型變量的內存都是何時釋放?

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

  引用類型:

  • 在沒有引用之后,通過 V8 自動回收。

  值類型:

  • 如果處於閉包的情況下,要等閉包沒有引用才會被 V8 回收。
  • 非閉包的情況下,等待 V8 的新生代切換的時候回收。

哪些情況會導致內存泄露?如何避免?

內存泄露是指你用不到(訪問不到)的變量,依然占居着內存空間,不能被再次利用起來。

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

  •  監聽在 window/body 等事件沒有解綁
  • 綁在 EventBus 的事件沒有解綁
  • Vuex 的 $storewatch 了之后沒有 unwatch
  • 使用第三方庫創建,沒有調用正確的銷毀函數

  解決辦法:beforeDestroy 中及時銷毀

  • 綁定了 DOM/BOM 對象中的事件 addEventListener ,removeEventListener
  • 觀察者模式 $on$off處理。
  • 如果組件中使用了定時器,應銷毀處理。
  • 如果在 mounted/created 鈎子中使用了第三方庫初始化,對應的銷毀。
  • 使用弱引用 weakMapweakSet

weakMap weakSet 

  在 ES6 中為我們新增了兩個數據結構 WeakMap、WeakSet,就是為了解決內存泄漏的問題。

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

  只要所引用的對象的其他引用都被清除,垃圾回收機制就會釋放該對象所占用的內存。

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


免責聲明!

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



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