學習筆記-瀏覽器的垃圾回收

1:瀏覽器怎么進行垃圾回收?
從三個點來回答什么是垃圾、如何撿垃圾、什么時候撿垃圾。
1.什么是垃圾

  • 不再需要,即為垃圾
  • 全局變量隨時可能用到,所以一定不是垃圾

2.如何撿垃圾(遍歷算法)

  • 標記空間中「可達」值。
    -根節(jié)點(Root)出發(fā),遍歷所有的對象。
    -可以遍歷到的對象,是可達的(reachable)。
    -沒有被遍歷到的對象,不可達的(unreachable)
  • 回收「不可達」的值所占據(jù)的內(nèi)存。
  • 做內(nèi)存整理。

3.什么時候撿垃圾

  • 前端有其特殊性,垃圾回收的時候會造成頁面卡頓。
  • 分代收集、增量收集、閑時收集。

2:瀏覽器中不同類型變量的內(nèi)存都是何時釋放?
Javascritp 中類型:值類型,引用類型。

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

3:哪些情況會導致內(nèi)存泄露?如何避免?

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

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

  • 監(jiān)聽在 window/body 等事件沒有解綁
  • 綁在 EventBus 的事件沒有解綁
  • Vuex 的 $store,watch 了之后沒有 unwatch
  • 使用第三方庫創(chuàng)建,沒有調(diào)用正確的銷毀函數(shù)

解決辦法:beforeDestroy 中及時銷毀

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

閉包會導致內(nèi)存泄露嗎?

順便說一個我在了解垃圾回收之前對閉包的誤解。
閉包會導致內(nèi)存泄露嗎?正確的答案是不會。
內(nèi)存泄露是指你「用不到」(訪問不到)的變量,依然占居著內(nèi)存空間,不能被再次利用起來。
閉包里面的變量就是我們需要的變量,不能說是內(nèi)存泄露。
這個誤解是如何來的?因為 IE。IE 有 bug,IE 在我們使用完閉包之后,依然回收不了閉包里面引用的變量。這是 IE 的問題,不是閉包的問題。

4:weakMap weakSet 和 Map Set 有什么區(qū)別?
在 ES6 中為我們新增了兩個數(shù)據(jù)結(jié)構(gòu) WeakMap、WeakSet,就是為了解決內(nèi)存泄漏的問題。
它的鍵名所引用的對象都是弱引用,就是垃圾回收機制遍歷的時候不考慮該引用。
只要所引用的對象的其他引用都被清除,垃圾回收機制就會釋放該對象所占用的內(nèi)存。
也就是說,一旦不再需要,WeakMap 里面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用。

了解瀏覽器垃圾回收的過程

  • 什么是垃圾數(shù)據(jù)?

生活中你買了一瓶可樂,喝完之后可樂瓶就變成了垃圾,應該被回收處理。
同樣地,我們在寫 js 代碼的時候,會頻繁地操作數(shù)據(jù)。
在一些數(shù)據(jù)不被需要的時候,它就是垃圾數(shù)據(jù),垃圾數(shù)據(jù)占用的內(nèi)存就應該被回收。

  • 變量的生命周期
    比如這么一段代碼:
let dog = new Object()let dog.a = new Array(1)

當 JavaScript 執(zhí)行這段代碼的時候,
會先在全局作用域中添加一個dog 屬性,并在堆中創(chuàng)建了一個空對象,將該對象的地址指向了 dog。
隨后又創(chuàng)建一個大小為 1 的數(shù)組,并將屬性地址指向了 dog.a。此時的內(nèi)存布局圖如下所示:


1.jpg

如果此時,我將另外一個對象賦給了 a 屬性,代碼如下所示:

dog.a = new Object()

此時的內(nèi)存布局圖:


2.jpg

a 的指向改變了, 此時堆中的數(shù)組對象就成為了不被使用的數(shù)據(jù),專業(yè)名詞叫「不可達」的數(shù)據(jù)。
這就是需要回收的垃圾數(shù)據(jù)。

  • 垃圾回收算法
    可以將這個過程想象成從根溢出一個巨大的油漆桶,它從一個根節(jié)點出發(fā)將可到達的對象標記染色, 然后移除未標記的。

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

V8 采用的是可達性 (reachability) 算法來判斷堆中的對象應不應該被回收。
這個算法的思路是這樣的:

從根節(jié)點(Root)出發(fā),遍歷所有的對象。
可以遍歷到的對象,是可達的(reachable)。
沒有被遍歷到的對象,不可達的(unreachable)。

在瀏覽器環(huán)境下,根節(jié)點有很多,主要包括這幾種:

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

這些根節(jié)點不是垃圾,不可能被回收。
第二步:回收「不可達」的值所占據(jù)的內(nèi)存。
在所有的標記完成之后,統(tǒng)一清理內(nèi)存中所有不可達的對象。
第三步,做內(nèi)存整理。

在頻繁回收對象后,內(nèi)存中就會存在大量不連續(xù)空間,專業(yè)名詞叫「內(nèi)存碎片」。
當內(nèi)存中出現(xiàn)了大量的內(nèi)存碎片,如果需要分配較大的連續(xù)內(nèi)存時,就有可能出現(xiàn)內(nèi)存不足的情況。
所以最后一步是整理內(nèi)存碎片。(但這步其實是可選的,因為有的垃圾回收器不會產(chǎn)生內(nèi)存碎片,比如接下來我們要介紹的副垃圾回收器。)

什么時候垃圾回收?

瀏覽器進行垃圾回收的時候,會暫停 JavaScript 腳本,等垃圾回收完畢再繼續(xù)執(zhí)行。
對于普通應用這樣沒什么問題,但對于 JS 游戲、動畫對連貫性要求比較高的應用,如果暫停時間很長就會造成頁面卡頓。
這就是我們接下來談的關(guān)于垃圾回收的問題:什么時候進行垃圾回收,可以避免長時間暫停。

分代收集

瀏覽器將數(shù)據(jù)分為兩種,一種是「臨時」對象,一種是「長久」對象。

  • 臨時對象:

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

  • 長久對象:

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

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

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

這樣就可以實現(xiàn)高效的垃圾回收啦。

主垃圾回收器

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

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

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

  1. 首先是標記。
  • 從一組根元素開始,遞歸遍歷這組根元素。
  • 在這個遍歷過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾數(shù)據(jù)。
  1. 然后是垃圾清除。


    3.jpg

    直接將標記為垃圾的數(shù)據(jù)清理掉。

  2. 多次標記-清除后,會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,需要進行內(nèi)存整理。


    4.jpg

副垃圾回收器

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

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


5.jpg

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

  1. 先給對象區(qū)域所有垃圾做標記。
  2. 標記完成后,存活的對象被復制到空閑區(qū)域,并且將他們有序的排列一遍。


    6.jpg

    這就回到我們前面留下的問題 -- 副垃圾回收器沒有碎片整理。因為空閑區(qū)域里此時是有序的,沒有碎片,也就不需要整理了。

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


    7.jpg

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

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

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

分代收集

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

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


8.jpg

增量收集

如果腳本中有許多對象,引擎一次性遍歷整個對象,會造成一個長時間暫停。
所以引擎將垃圾收集工作分成更小的塊,每次處理一部分,多次處理。
這樣就解決了長時間停頓的問題。


9.jpg

閑時收集

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容