【七】垃圾回收機(jī)制

前言
本篇章講述關(guān)于垃圾回收機(jī)制、內(nèi)存泄漏以及堆棧溢出的相關(guān)知識(shí),并了解如何通過工具定位排查內(nèi)存泄漏情況,實(shí)現(xiàn)性能優(yōu)化。
面試回答
1.垃圾回收機(jī)制:垃圾回收機(jī)制就是周期性地找出不再繼續(xù)使用的變量,釋放其內(nèi)存。那么通常相關(guān)的問題就是內(nèi)存泄漏問題,原因就是被占用的內(nèi)存由于程序原因無法釋放,造成浪費(fèi),如果內(nèi)存泄漏過多的話,會(huì)使程序無法申請(qǐng)到內(nèi)存,出現(xiàn)程序緩慢甚至崩潰的情況,那么通常的解決辦法就是把這些隱患消滅在開發(fā)階段,比如避免意外的全局變量、及時(shí)關(guān)閉定時(shí)器、釋放DOM引用、釋放監(jiān)聽事件以及減少閉包的使用。
知識(shí)點(diǎn)
1.垃圾回收機(jī)制
垃圾回收機(jī)制即周期性地找出不再繼續(xù)使用的變量,釋放其內(nèi)存。判斷變量是否不再使用的條件是通過標(biāo)簽判斷,標(biāo)識(shí)無用變量的策略有以下的2種方式。
1.標(biāo)記清除
絕大多數(shù)瀏覽器采用的垃圾收集機(jī)制均是通過標(biāo)記清除的方式,它主要分為兩個(gè)階段,標(biāo)記階段和清除階段,判斷標(biāo)準(zhǔn)是看這個(gè)對(duì)象是否可抵達(dá)。
標(biāo)記階段:垃圾收集器會(huì)從根對(duì)象(Window對(duì)象)出發(fā),掃描所有可以觸及的對(duì)象,這就是所謂的可抵達(dá)。
清除階段:在掃描的同時(shí),根對(duì)象無法觸及(不可抵達(dá))的對(duì)象,就是被認(rèn)為不被需要的對(duì)象,就會(huì)被當(dāng)成垃圾清除。
在函數(shù)執(zhí)行完畢之后,函數(shù)的聲明周期結(jié)束,那么現(xiàn)在,從?Window對(duì)象
?出發(fā),?obj1
?和?obj2
?都會(huì)被垃圾收集器標(biāo)記為不可抵達(dá)(因?yàn)榉椒ㄒ呀?jīng)執(zhí)行過,掃描就到達(dá)不了方法內(nèi)部,自然也就掃描不到obj對(duì)象),這樣子的情況下,互相引用的情況也會(huì)迎刃而解。
2.引用計(jì)數(shù)
引用計(jì)數(shù)法,就是變量引用的次數(shù)。你可以認(rèn)為它就是對(duì)當(dāng)前變量所引用次數(shù)的描述。
當(dāng)給obj賦值的同時(shí),其實(shí)就創(chuàng)建了一個(gè)指向該變量的引用,引用計(jì)數(shù)為1,在引用計(jì)數(shù)法的機(jī)制下,內(nèi)存中的每一個(gè)值都會(huì)對(duì)應(yīng)一個(gè)引用計(jì)數(shù)。 而當(dāng)我們給obj賦值為null時(shí),這個(gè)變量就變成了一塊沒用的內(nèi)存,那么此時(shí),obj的引用計(jì)數(shù)將會(huì)變成?0,所有引用計(jì)數(shù)為0的變量都將會(huì)被垃圾收集器所回收,然后obj所占用的內(nèi)存空間將會(huì)被釋放。
2.內(nèi)存泄漏
內(nèi)存泄漏(Memory Leak)是指程序中已動(dòng)態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),內(nèi)存泄露過多的話,就會(huì)導(dǎo)致后面的程序申請(qǐng)不到內(nèi)存,因此內(nèi)存泄露會(huì)導(dǎo)致內(nèi)部內(nèi)存溢出,導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。
1.意外的全局變量
在JavaScript中并未嚴(yán)格定義對(duì)未聲明變量的處理方式,即使在局部函數(shù)作用域中依舊能夠定義全局變量,這種意外的全局變量可能會(huì)存儲(chǔ)大量數(shù)據(jù),且由于其是能夠通過全局對(duì)象例如window能夠訪問到的,所以進(jìn)行內(nèi)存回收時(shí)不認(rèn)為其是需要回收的內(nèi)存而一直存在,只有在窗口關(guān)閉或者刷新頁面時(shí)才能夠被釋放,造成意外的內(nèi)存泄漏。
解決方式
使用嚴(yán)格模式("use strict"),使用let const 來定義變量,嚴(yán)格模式下定義未聲明變量在會(huì)拋出錯(cuò)誤;
減少創(chuàng)建全局變量,如果必須使用全局變量存儲(chǔ)大量數(shù)據(jù),確保使用完以后把他設(shè)置為
null
或者重新定義。
2.被遺忘的定時(shí)器和回調(diào)函數(shù)
當(dāng)不需要setInterval或者setTimeout時(shí),定時(shí)器沒有被clear,定時(shí)器的回調(diào)函數(shù)以及內(nèi)部依賴的變量都不能被回收,造成內(nèi)存泄漏。
這段代碼是在點(diǎn)擊按鈕后執(zhí)行fn1函數(shù),fn1函數(shù)內(nèi)創(chuàng)建了一個(gè)很大的數(shù)組對(duì)象largeObj,同時(shí)創(chuàng)建了一個(gè)setInterval定時(shí)器,定時(shí)器的回調(diào)函數(shù)只是簡(jiǎn)單的引用了一下變量largeObj,但是從 Chrome devTools 查看還是能看出內(nèi)存泄漏。原因其實(shí)就是因?yàn)閟etInterval的回調(diào)函數(shù)內(nèi)對(duì)變量largeObj有一個(gè)引用關(guān)系,而定時(shí)器一直未被清除,所以變量largeObj的內(nèi)存也自然不會(huì)被釋放。
驗(yàn)證方式:打開控制臺(tái),點(diǎn)擊Performacne,點(diǎn)擊圓點(diǎn),每個(gè)一段時(shí)間點(diǎn)擊按鈕,如果存在內(nèi)存泄漏的情況會(huì)出現(xiàn)這種階梯狀

正常的則是單個(gè),如下圖。

解決方式
設(shè)置關(guān)閉條件,使用clearInterval、clearTimeout釋放
3.分離的DOM節(jié)點(diǎn)
假設(shè)你手動(dòng)移除了某個(gè)dom節(jié)點(diǎn),本應(yīng)釋放該dom節(jié)點(diǎn)所占用的內(nèi)存,但卻因?yàn)槭韬鰧?dǎo)致某處代碼仍對(duì)該被移除節(jié)點(diǎn)有引用,最終導(dǎo)致該節(jié)點(diǎn)所占內(nèi)存無法被釋放,例如這種情況:
該代碼所做的操作就是點(diǎn)擊按鈕后移除.child的節(jié)點(diǎn),雖然點(diǎn)擊后,該節(jié)點(diǎn)確實(shí)從dom被移除了,但全局變量child仍對(duì)該節(jié)點(diǎn)有引用,所以導(dǎo)致該節(jié)點(diǎn)的內(nèi)存一直無法被釋放。
解決方式
改動(dòng)很簡(jiǎn)單,就是將對(duì).child節(jié)點(diǎn)的引用移動(dòng)到了click事件的回調(diào)函數(shù)中,那么當(dāng)移除節(jié)點(diǎn)并退出回調(diào)函數(shù)的執(zhí)行上文后就會(huì)自動(dòng)清除對(duì)該節(jié)點(diǎn)的引用,那么自然就不會(huì)存在內(nèi)存泄漏的情況了。
4.閉包使用不當(dāng)
下面的例子中,在test函數(shù)執(zhí)行上下文后,該上下文中的變量a本應(yīng)被當(dāng)作垃圾數(shù)據(jù)給回收掉,但因test函數(shù)最終將變量a返回并賦值給全局變量res,其產(chǎn)生了對(duì)變量a的引用,所以變量a被標(biāo)記為活動(dòng)變量并一直占用著相應(yīng)的內(nèi)存,假設(shè)變量res后續(xù)用不到,這就算是一種閉包使用不當(dāng)?shù)睦印?/span>
解決方式
3.堆棧溢出
堆棧溢出 :每次執(zhí)行JavaScript代碼時(shí),都會(huì)分配一定尺寸的??臻g(Windows系統(tǒng)中為1M),每次方法調(diào)用時(shí)都會(huì)在棧里儲(chǔ)存一定信息(如參數(shù)、局部變量、返回值等等),這些信息再少也會(huì)占用一定空間,如果存在較多的此類空間,就會(huì)超過線程的??臻g了。堆棧溢出很可能由無限遞歸產(chǎn)生,但也可能僅僅是過多的堆棧層級(jí)。 說白了就是就是不顧堆棧中分配的局部數(shù)據(jù)塊大小,向該數(shù)據(jù)塊寫入了過多的數(shù)據(jù),導(dǎo)致數(shù)據(jù)越界,結(jié)果覆蓋了別的數(shù)據(jù)。如下示例,便會(huì)報(bào)錯(cuò),這是由于過多的函數(shù)調(diào)用,導(dǎo)致調(diào)用堆棧無法容納這些調(diào)用的返回地址,一般在遞歸中產(chǎn)生。
解決方式:
1.遞歸改為循環(huán)
優(yōu)化原理:所有運(yùn)算均在一個(gè)執(zhí)行上下文中執(zhí)行,不用生成額外的上下文。
2.使用閉包
3.使用setTimeout()來解決
堆棧溢出之所以會(huì)被消除,是因?yàn)槭录h(huán)操縱了遞歸,而不是調(diào)用堆棧(也就是執(zhí)行棧)。思路就是不把遞歸里的函數(shù)放到調(diào)用棧里,比如通過setTimeout(宏任務(wù))丟到任務(wù)隊(duì)列中然后按照事件循環(huán)來控制,但是這樣的就會(huì)有作用域及this指針的問題,需要修改一些業(yè)務(wù)邏輯,而且調(diào)用有一個(gè)最小的時(shí)間間隔,又是異步的,即時(shí)性也不好。
4.通過promise來處理
使用promise把遞歸放到微任務(wù)里執(zhí)行,原理與setTimeout一致,只不過一個(gè)是靠宏任務(wù)(setTimeout),一個(gè)是靠微任務(wù)(Promise),通過時(shí)間循環(huán)來解決遞歸里的調(diào)用棧問題。
5.尾調(diào)用優(yōu)化
ECMAScript 6 規(guī)范新增了一項(xiàng)內(nèi)存管理優(yōu)化機(jī)制,讓 JavaScript 引擎在滿足條件時(shí)可以重用棧幀。尾調(diào)用優(yōu)化的條件就是確定外部棧幀真的沒必要存在了
代碼在嚴(yán)格模式下執(zhí)行
外部函數(shù)的返回值是對(duì)尾調(diào)用函數(shù)的調(diào)用
尾調(diào)用函數(shù)返回后不需要執(zhí)行額外的邏輯
尾調(diào)用函數(shù)不是引用外部函數(shù)作用域中自由變量的閉包
最后
走過路過,不要錯(cuò)過,點(diǎn)贊、收藏、評(píng)論三連~