【面試題】727- 從 4 個(gè)面試題了解「瀏覽器的垃圾回收」

來源:Marica
https://juejin.im/post/6861967094318284814
瀏覽器垃圾回收一直是前端面試常考的部分,我一直不太理解。最近深入學(xué)習(xí)了一下,爭取一篇文章說清楚。
我們首先帶著這 4 個(gè)問題,來了解瀏覽器垃圾回收的過程,后面會(huì)逐一解答:
瀏覽器怎么進(jìn)行垃圾回收? 瀏覽器中不同類型變量的內(nèi)存都是何時(shí)釋放? 哪些情況會(huì)導(dǎo)致內(nèi)存泄露?如何避免? weakMapweakSet和MapSet有什么區(qū)別?
ok, let's go!
什么是垃圾數(shù)據(jù)?
生活中你買了一瓶可樂,喝完之后可樂瓶就變成了垃圾,應(yīng)該被回收處理。
同樣地,我們?cè)趯?js 代碼的時(shí)候,會(huì)頻繁地操作數(shù)據(jù)。
在一些數(shù)據(jù)不被需要的時(shí)候,它就是垃圾數(shù)據(jù),垃圾數(shù)據(jù)占用的內(nèi)存就應(yīng)該被回收。
變量的生命周期
比如這么一段代碼:
let?dog?=?new?Object()let?dog.a?=?new?Array(1)
當(dāng) JavaScript 執(zhí)行這段代碼的時(shí)候,
會(huì)先在全局作用域中添加一個(gè)dog 屬性,并在堆中創(chuàng)建了一個(gè)空對(duì)象,將該對(duì)象的地址指向了 dog。
隨后又創(chuàng)建一個(gè)大小為 1 的數(shù)組,并將屬性地址指向了 dog.a。此時(shí)的內(nèi)存布局圖如下所示:
如果此時(shí),我將另外一個(gè)對(duì)象賦給了 a 屬性,代碼如下所示:
dog.a?=?new?Object()復(fù)制代碼
此時(shí)的內(nèi)存布局圖:
a 的指向改變了, 此時(shí)堆中的數(shù)組對(duì)象就成為了不被使用的數(shù)據(jù),專業(yè)名詞叫「不可達(dá)」的數(shù)據(jù)。
這就是需要回收的垃圾數(shù)據(jù)。
垃圾回收算法
可以將這個(gè)過程想象成從根溢出一個(gè)巨大的油漆桶,它從一個(gè)根節(jié)點(diǎn)出發(fā)將可到達(dá)的對(duì)象標(biāo)記染色, 然后移除未標(biāo)記的。
第一步:標(biāo)記空間中「可達(dá)」值。
V8 采用的是可達(dá)性 (reachability) 算法來判斷堆中的對(duì)象應(yīng)不應(yīng)該被回收。
這個(gè)算法的思路是這樣的:
從根節(jié)點(diǎn)(Root)出發(fā),遍歷所有的對(duì)象。 可以遍歷到的對(duì)象,是可達(dá)的(reachable)。 沒有被遍歷到的對(duì)象,不可達(dá)的(unreachable)。
在瀏覽器環(huán)境下,根節(jié)點(diǎn)有很多,主要包括這幾種:
全局變量 window,位于每個(gè)iframe中文檔 DOM樹存放在棧上的變量 ...
這些根節(jié)點(diǎn)不是垃圾,不可能被回收。
第二步:回收「不可達(dá)」的值所占據(jù)的內(nèi)存。
在所有的標(biāo)記完成之后,統(tǒng)一清理內(nèi)存中所有不可達(dá)的對(duì)象。
第三步,做內(nèi)存整理。
在頻繁回收對(duì)象后,內(nèi)存中就會(huì)存在大量不連續(xù)空間,專業(yè)名詞叫「內(nèi)存碎片」。 當(dāng)內(nèi)存中出現(xiàn)了大量的內(nèi)存碎片,如果需要分配較大的連續(xù)內(nèi)存時(shí),就有可能出現(xiàn)內(nèi)存不足的情況。 所以最后一步是整理內(nèi)存碎片。(但這步其實(shí)是可選的,因?yàn)橛械睦厥掌鞑粫?huì)產(chǎn)生內(nèi)存碎片,比如接下來我們要介紹的副垃圾回收器。)
什么時(shí)候垃圾回收?
瀏覽器進(jìn)行垃圾回收的時(shí)候,會(huì)暫停 JavaScript 腳本,等垃圾回收完畢再繼續(xù)執(zhí)行。
對(duì)于普通應(yīng)用這樣沒什么問題,但對(duì)于 JS 游戲、動(dòng)畫對(duì)連貫性要求比較高的應(yīng)用,如果暫停時(shí)間很長就會(huì)造成頁面卡頓。
這就是我們接下來談的關(guān)于垃圾回收的問題:什么時(shí)候進(jìn)行垃圾回收,可以避免長時(shí)間暫停。
分代收集
瀏覽器將數(shù)據(jù)分為兩種,一種是「臨時(shí)」對(duì)象,一種是「長久」對(duì)象。
臨時(shí)對(duì)象:
大部分對(duì)象在內(nèi)存中存活的時(shí)間很短。 比如函數(shù)內(nèi)部聲明的變量,或者塊級(jí)作用域中的變量。當(dāng)函數(shù)或者代碼塊執(zhí)行結(jié)束時(shí),作用域中定義的變量就會(huì)被銷毀。 這類對(duì)象很快就變得不可訪問,應(yīng)該快點(diǎn)回收。 長久對(duì)象:
生命周期很長的對(duì)象,比如全局的 window、DOM、Web API等等。這類對(duì)象可以慢點(diǎn)回收。
這兩種對(duì)象對(duì)應(yīng)不同的回收策略,所以,V8 把堆分為新生代和老生代兩個(gè)區(qū)域, 新生代中存放臨時(shí)對(duì)象,老生代中存放持久對(duì)象。
并且讓副垃圾回收器、主垃圾回收器,分別負(fù)責(zé)新生代、老生代的垃圾回收。
這樣就可以實(shí)現(xiàn)高效的垃圾回收啦。
一般來說,面試回答到這就夠了。如果想和面試官深入交流,可以繼續(xù)聊聊兩個(gè)垃圾回收器。
主垃圾回收器
負(fù)責(zé)老生代的垃圾回收,有兩個(gè)特點(diǎn):
對(duì)象占用空間大。 對(duì)象存活時(shí)間長。
它使用「標(biāo)記-清除」的算法執(zhí)行垃圾回收。
首先是標(biāo)記。
從一組根元素開始,遞歸遍歷這組根元素。 在這個(gè)遍歷過程中,能到達(dá)的元素稱為活動(dòng)對(duì)象,沒有到達(dá)的元素就可以判斷為垃圾數(shù)據(jù)。 然后是垃圾清除。
直接將標(biāo)記為垃圾的數(shù)據(jù)清理掉。多次標(biāo)記-清除后,會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,需要進(jìn)行內(nèi)存整理。

副垃圾回收器
負(fù)責(zé)新生代的垃圾回收,通常只支持 1~8 M 的容量。
新生代被分為兩個(gè)區(qū)域:一般是對(duì)象區(qū)域,一半是空閑區(qū)域。
新加入的對(duì)象都被放入對(duì)象區(qū)域,等對(duì)象區(qū)域快滿的時(shí)候,會(huì)執(zhí)行一次垃圾清理。
先給對(duì)象區(qū)域所有垃圾做標(biāo)記。 標(biāo)記完成后,存活的對(duì)象被復(fù)制到空閑區(qū)域,并且將他們有序的排列一遍。
這就回到我們前面留下的問題 -- 副垃圾回收器沒有碎片整理。因?yàn)榭臻e區(qū)域里此時(shí)是有序的,沒有碎片,也就不需要整理了。復(fù)制完成后,對(duì)象區(qū)域會(huì)和空閑區(qū)域進(jìn)行對(duì)調(diào)。將空閑區(qū)域中存活的對(duì)象放入對(duì)象區(qū)域里。
這樣,就完成了垃圾回收。
因?yàn)楦崩厥掌鞑僮鞅容^頻繁,所以為了執(zhí)行效率,一般新生區(qū)的空間會(huì)被設(shè)置得比較小。
一旦檢測(cè)到空間裝滿了,就執(zhí)行垃圾回收。
分代收集
一句話總結(jié)分代回收就是:將堆分為新生代與老生代,多回收新生代,少回收老生代。
這樣就減少了每次需遍歷的對(duì)象,從而減少每次垃圾回收的耗時(shí)。
增量收集
如果腳本中有許多對(duì)象,引擎一次性遍歷整個(gè)對(duì)象,會(huì)造成一個(gè)長時(shí)間暫停。
所以引擎將垃圾收集工作分成更小的塊,每次處理一部分,多次處理。
這樣就解決了長時(shí)間停頓的問題。
閑時(shí)收集
垃圾收集器只會(huì)在 CPU 空閑時(shí)嘗試運(yùn)行,以減少可能對(duì)代碼執(zhí)行的影響。
面試題1:瀏覽器怎么進(jìn)行垃圾回收?
從三個(gè)點(diǎn)來回答什么是垃圾、如何撿垃圾、什么時(shí)候撿垃圾。
什么是垃圾
不再需要,即為垃圾 全局變量隨時(shí)可能用到,所以一定不是垃圾 如何撿垃圾(遍歷算法)
標(biāo)記空間中「可達(dá)」值。
- 從根節(jié)點(diǎn)(Root)出發(fā),遍歷所有的對(duì)象。
- 可以遍歷到的對(duì)象,是可達(dá)的(reachable)。
- 沒有被遍歷到的對(duì)象,不可達(dá)的(unreachable)回收「不可達(dá)」的值所占據(jù)的內(nèi)存。
做內(nèi)存整理。
什么時(shí)候撿垃圾
前端有其特殊性,垃圾回收的時(shí)候會(huì)造成頁面卡頓。 分代收集、增量收集、閑時(shí)收集。
面試題2:瀏覽器中不同類型變量的內(nèi)存都是何時(shí)釋放?
Javascritp 中類型:值類型,引用類型。
引用類型
在沒有引用之后,通過 V8 自動(dòng)回收。 值類型
如果處于閉包的情況下,要等閉包沒有引用才會(huì)被 V8 回收。 非閉包的情況下,等待 V8 的新生代切換的時(shí)候回收。
面試題3:哪些情況會(huì)導(dǎo)致內(nèi)存泄露?如何避免?
內(nèi)存泄露是指你「用不到」(訪問不到)的變量,依然占居著內(nèi)存空間,不能被再次利用起來。
以 Vue 為例,通常有這些情況:
監(jiān)聽在 window/body等事件沒有解綁綁在 EventBus的事件沒有解綁Vuex的$store,watch了之后沒有unwatch使用第三方庫創(chuàng)建,沒有調(diào)用正確的銷毀函數(shù)
解決辦法:beforeDestroy 中及時(shí)銷毀
綁定了 DOM/BOM對(duì)象中的事件addEventListener,removeEventListener。觀察者模式 $on,$off處理。如果組件中使用了定時(shí)器,應(yīng)銷毀處理。 如果在 mounted/created鉤子中使用了第三方庫初始化,對(duì)應(yīng)的銷毀。使用弱引用 weakMap、weakSet。
閉包會(huì)導(dǎo)致內(nèi)存泄露嗎?
順便說一個(gè)我在了解垃圾回收之前對(duì)閉包的誤解。
閉包會(huì)導(dǎo)致內(nèi)存泄露嗎?正確的答案是不會(huì)。
內(nèi)存泄露是指你「用不到」(訪問不到)的變量,依然占居著內(nèi)存空間,不能被再次利用起來。
閉包里面的變量就是我們需要的變量,不能說是內(nèi)存泄露。
這個(gè)誤解是如何來的?因?yàn)?IE。IE 有 bug,IE 在我們使用完閉包之后,依然回收不了閉包里面引用的變量。這是 IE 的問題,不是閉包的問題。參考這篇文章
面試題4:weakMap weakSet 和 Map Set 有什么區(qū)別?
在 ES6 中為我們新增了兩個(gè)數(shù)據(jù)結(jié)構(gòu) WeakMap、WeakSet,就是為了解決內(nèi)存泄漏的問題。
它的鍵名所引用的對(duì)象都是弱引用,就是垃圾回收機(jī)制遍歷的時(shí)候不考慮該引用。
只要所引用的對(duì)象的其他引用都被清除,垃圾回收機(jī)制就會(huì)釋放該對(duì)象所占用的內(nèi)存。
也就是說,一旦不再需要,WeakMap 里面的鍵名對(duì)象和所對(duì)應(yīng)的鍵值對(duì)會(huì)自動(dòng)消失,不用手動(dòng)刪除引用。
更全面的介紹可以看這里:第 4 題:介紹下 Set、Map、WeakSet 和 WeakMap 的區(qū)別
總結(jié)
現(xiàn)在我們簡單了解了瀏覽器的垃圾回收機(jī)制,還記得最初的 4 個(gè)問題嗎?
瀏覽器怎么進(jìn)行垃圾回收?
答題思路:什么是垃圾、怎么收垃圾、什么時(shí)候收垃圾。
瀏覽器中不同類型變量的內(nèi)存都是何時(shí)釋放?
答題思路:分為值類型、引用類型。
哪些情況會(huì)導(dǎo)致內(nèi)存泄露?如何避免?
答題思路:內(nèi)存泄露是指你「用不到」(訪問不到)的變量,依然占居著內(nèi)存空間,不能被再次利用起來。
weakMapweakSet和MapSet有什么區(qū)別?
答題思路:WeakMap、WeakSet 弱引用,解決了內(nèi)存泄露問題。

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章
