您好,登錄后才能下訂單哦!
這篇文章主要講解了“怎么排查Javascript內(nèi)存泄漏”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“怎么排查Javascript內(nèi)存泄漏”吧!
為了證明螃蟹的聽(tīng)覺(jué)在腿上,一個(gè)專家捉了只螃蟹并沖它大吼,螃蟹很快就跑了。然后捉回來(lái)再?zèng)_它吼,螃蟹又跑了。最后專家把螃蟹的腿都切了,又對(duì)著螃蟹大吼,螃蟹果然一動(dòng)不動(dòng)……
定位內(nèi)存問(wèn)題的過(guò)程其實(shí)也類似,如果你自己都不知道自己的頁(yè)面在使用過(guò)程中哪些步驟會(huì)導(dǎo)致內(nèi)存增長(zhǎng),那很可能就會(huì)錯(cuò)把一個(gè)正常的內(nèi)存增長(zhǎng)當(dāng)作內(nèi)存泄漏來(lái)排查,最后查了半天白忙活。 其實(shí)一個(gè)單頁(yè)應(yīng)用在使用過(guò)程中,內(nèi)存發(fā)生增長(zhǎng)是很合理的。例如在開(kāi)發(fā)過(guò)程中,為了優(yōu)化使用體驗(yàn),我們可能會(huì)對(duì)部分?jǐn)?shù)據(jù)進(jìn)行緩存,這部分緩存的數(shù)據(jù)其實(shí)也會(huì)導(dǎo)致內(nèi)存占用的升高,但它是符合預(yù)期的。因此,排查內(nèi)存泄漏的第一步,就是要先梳理一遍自己的代碼,看一下哪部分內(nèi)存的升高是合理的,哪部分內(nèi)存的升高是不合理的。
答案是先用Performance。 當(dāng)我們懷疑頁(yè)面發(fā)生了內(nèi)存泄漏的時(shí)候,可以先用Performance錄制一段時(shí)間內(nèi)頁(yè)面的性能變化。你只需要切換到Performance面板,點(diǎn)擊Record,然后在頁(yè)面上正常操作一段時(shí)間,最后停止錄制即可。
不斷升高的內(nèi)存下限
如果錄制結(jié)束后,看到內(nèi)存的下限在不斷升高的話,你就要注意了 —— 這里有可能發(fā)生了內(nèi)存泄漏。
除了內(nèi)存增長(zhǎng)曲線,Nodes(Dom節(jié)點(diǎn)數(shù)曲線)、Document曲線以及Listener曲線也同樣值得關(guān)注,有時(shí)候它們對(duì)內(nèi)存問(wèn)題的定位也很有幫助。
當(dāng)你懷疑發(fā)生了內(nèi)存泄漏的時(shí)候,你就可以用Memory面板來(lái)進(jìn)一步定位泄漏的源頭了。
通常,我們可以從Memory的主界面開(kāi)始,點(diǎn)擊左上角的圓點(diǎn)就可以記錄下當(dāng)前的堆內(nèi)存快照(heap snapshot)了。
Memory面板
這里推薦一個(gè)Gmail團(tuán)隊(duì)也在用的 “three snapshot”技巧:
打開(kāi)DevTools, 切換至Memory面板
先記錄一個(gè)堆內(nèi)存快照
在你的頁(yè)面上執(zhí)行可能發(fā)生泄漏的操作
再記錄一個(gè)堆內(nèi)存快照
重復(fù)執(zhí)行多幾遍步驟3
最后記錄一個(gè)堆內(nèi)存快照
選擇最后一個(gè)堆內(nèi)存快照,找到頂欄的“All objects”, 切換至”O(jiān)bjects allocated between snapshots 1 and 2”(也可以對(duì)2,3執(zhí)行同樣的操作)
過(guò)濾出兩份快照之間新分配的對(duì)象
8. 切換后,你就能看到兩個(gè)快照之間新生成的對(duì)象。你可以選擇其中一項(xiàng)點(diǎn)開(kāi),看看它的retaining tree里面保留了哪些對(duì)象沒(méi)有釋放。
Tips:在記錄第一個(gè)堆快照之前你可以先做一些“預(yù)熱”操作,避免一些懶加載和緩存策略影響到了對(duì)內(nèi)存的分析。
這也是我排查內(nèi)存泄漏時(shí)遇到的第一個(gè)問(wèn)題,為什么教程里的內(nèi)存快照簡(jiǎn)潔易懂,我的內(nèi)存快照卻像一本天書(shū)?
教程里的內(nèi)存快照
我的內(nèi)存快照
為什么有這么大的差異呢?除去教程里demo代碼比較簡(jiǎn)單之外,提前準(zhǔn)備好一個(gè)合理的debug環(huán)境也是很重要的。這里我列舉了4點(diǎn)個(gè)人覺(jué)得對(duì)debug內(nèi)存問(wèn)題很有幫助的措施:
1. 盡量使用沒(méi)有混淆的代碼:
打包后的代碼往往經(jīng)過(guò)了混淆和壓縮,在生產(chǎn)環(huán)境上這是必要的,但在debug時(shí)卻會(huì)成為我們的絆腳石,不便于閱讀。
2. 排查問(wèn)題時(shí)使用production模式編譯出來(lái)的代碼:
Dev模式下往往會(huì)開(kāi)啟一些方便開(kāi)發(fā)的特性,例如熱更新等。但它們可能會(huì)占用一部分的內(nèi)存,影響到內(nèi)存問(wèn)題的排查,所以建議還是使用production模式編譯出來(lái)的代碼進(jìn)行問(wèn)題排查。
3. 屏蔽所有瀏覽器插件:
屏蔽瀏覽器插件最快的方式就是打開(kāi)無(wú)痕窗口。瀏覽器插件給我們帶來(lái)很多便利,但插件注入的額外邏輯有時(shí)也會(huì)影響內(nèi)存問(wèn)題的排查。例如vue-devtools會(huì)記錄下每一個(gè)vuex mutaions,導(dǎo)致內(nèi)存無(wú)法釋放。
4. 在現(xiàn)場(chǎng)打內(nèi)存快照,便于跳轉(zhuǎn)到源代碼所在行:
盡管devTools記錄下來(lái)的內(nèi)存快照文件可以單獨(dú)加載展示,但還是建議在記錄下內(nèi)存快照的時(shí)候“趁熱”分析,因?yàn)檫@時(shí)還能從retaining tree上跳轉(zhuǎn)到代碼所在行,有時(shí)候?qū)Χㄎ粏?wèn)題也很有幫助。
跳轉(zhuǎn)到源碼所在行
一個(gè)DOM節(jié)點(diǎn)只有在沒(méi)有被頁(yè)面的DOM樹(shù)或者Javascript引用時(shí),才會(huì)被垃圾回收。當(dāng)一個(gè)節(jié)點(diǎn)處于“detached”狀態(tài),表示它已經(jīng)不在DOM樹(shù)上了,但Javascript仍舊對(duì)它有引用,所以暫時(shí)沒(méi)有被回收。通常,Detached DOM tree往往會(huì)造成內(nèi)存泄漏,我們可以重點(diǎn)分析這部分的數(shù)據(jù)。
Shallow size: 這是對(duì)象自身占用內(nèi)存的大小。通常只有數(shù)組和字符串的shallow size比較大。
Retain size: 這是將對(duì)象本身連同其無(wú)法從 GC 根到達(dá)的相關(guān)對(duì)象一起刪除后釋放的內(nèi)存大小。 因此,如果Shallow Size = Retained Size,說(shuō)明基本沒(méi)怎么泄漏。而如果Retained Size > Shallow Size,就需要多加注意了。
顧名思義,Summary view就是當(dāng)前內(nèi)存快照的一個(gè)概覽。我們先介紹一下這個(gè)視圖下的每一列是什么意思: - Constructor: 對(duì)象的構(gòu)造器。 - Distance:與root的距離。距離越大,處理和加載這個(gè)對(duì)象的時(shí)間就越長(zhǎng)。 - Object Count:指定構(gòu)造器創(chuàng)建的對(duì)象的數(shù)量。 - Shallow Size:對(duì)象自身占用內(nèi)存的大小。 - Retained Size:釋放掉該對(duì)象后,能釋放掉的內(nèi)存。
在這個(gè)視圖下你可以看到當(dāng)前頁(yè)面內(nèi)存的具體構(gòu)成,但如果想定位內(nèi)存問(wèn)題,下面的Comparison view會(huì)更加有用。
Comparison視圖可以讓你對(duì)比兩份內(nèi)存快照之間的差異。默認(rèn)是跟上一份快照做對(duì)比,當(dāng)然你也可以選擇任意兩份內(nèi)存做對(duì)比。這個(gè)視圖下每一列的數(shù)據(jù)有點(diǎn)不同: - Constructor: 對(duì)象的構(gòu)造器。 - # New: 該對(duì)象構(gòu)造器下有多少新對(duì)象被創(chuàng)建 - # Deleted: 該對(duì)象構(gòu)造器下有多少新對(duì)象被銷毀 - # Delta: # New - # Delete的差值 - Alloc.Size:兩份快照之間新分配的內(nèi)存 - Freed Size: 兩份快照之間釋放掉的內(nèi)存 - Size Delta:Alloc Size - Freed Size 的差值
這個(gè)視圖絕對(duì)是排查內(nèi)存泄漏的利器。當(dāng)你能定位到是哪些操作可能造成內(nèi)存泄漏后,比較操作前后的內(nèi)存快照,很容易就能發(fā)現(xiàn)發(fā)生內(nèi)存泄漏的對(duì)象。
Containment view提供了一個(gè)自下而上的視圖,它允許你瀏覽和探索堆內(nèi)存的內(nèi)容。我們可以用它來(lái)分析一些全部變量的引用情況(如window)。
Statistics視圖會(huì)用餅圖的形式展示各個(gè)類型對(duì)象的內(nèi)存占比
(closure): 函數(shù)閉包持有的內(nèi)存引用。
(array, string, number, regex): 包含著一系列對(duì)象,這些對(duì)象的屬性上有對(duì)應(yīng)類型變量的引用。
(compiled code): Javascript引擎(如V8)為了加快運(yùn)行速度,會(huì)對(duì)代碼進(jìn)行一次編譯。(compiled code)顧名思義就是指與編譯后的代碼相關(guān)聯(lián)的內(nèi)存。
Detached HTMLDivElement等:代碼里對(duì)指定類型Dom節(jié)點(diǎn)的引用。
經(jīng)常出現(xiàn)的feedback_cell
放心,它不會(huì)造成內(nèi)存泄漏。它是v8對(duì)頻繁運(yùn)行的熱代碼做出的優(yōu)化,會(huì)被v8自己回收。詳見(jiàn)這篇文章:Feedback vectors in heap snapshots – Rohit Pagariya
這里列舉了一些常見(jiàn)的內(nèi)存泄漏場(chǎng)景,遇到內(nèi)存泄漏問(wèn)題時(shí)可以先自查一遍常見(jiàn)場(chǎng)景,個(gè)人感覺(jué)能解決日常開(kāi)發(fā)中遇到的90%內(nèi)存泄漏
console導(dǎo)致的內(nèi)存泄漏 因?yàn)榇蛴『蟮膶?duì)象需要支持在控制臺(tái)上查看,所以傳遞給console.log方法的對(duì)象是不能被垃圾回收的。我們需要避免在生產(chǎn)環(huán)境用console打印對(duì)象。
框架配合第三方庫(kù)使用時(shí),沒(méi)有及時(shí)執(zhí)行銷毀 這點(diǎn)可以參考vue cookbook里的例子
被遺忘的定時(shí)器 例如在組件初始化的時(shí)候設(shè)置了setInterval
,那么在組件銷毀之前記得調(diào)用clearInterval
方法取消定時(shí)器。
沒(méi)有正確移除事件監(jiān)聽(tīng)器(各種EventBus, dom事件監(jiān)聽(tīng)等) 這應(yīng)該是最容易犯的一個(gè)錯(cuò)誤,無(wú)論新手老手都有可能栽在這里。
特征:performance里,監(jiān)聽(tīng)器數(shù)量會(huì)持續(xù)上升
持續(xù)上升的監(jiān)聽(tīng)器數(shù)量
啰嗦一句:盡管大部分同學(xué)都會(huì)有主動(dòng)移除監(jiān)聽(tīng)器的觀念,但如果姿勢(shì)不對(duì),可能依舊會(huì)造成內(nèi)存泄漏。下面是一個(gè)真實(shí)案例:
// 版本一 mounted() { window.addEventListener('resize', debounce(this.handleWidthChange, 100)) }, beforeDestroy() { window.removeEventListener('resize', debounce(this.handleWidthChange, 100)) }
乍一看好像寫(xiě)的還不錯(cuò),有及時(shí)移除監(jiān)聽(tīng)器,對(duì)resize這種頻繁觸發(fā)的事件也加了debounce處理。但其實(shí)這段代碼就導(dǎo)致了內(nèi)存泄漏:每次調(diào)用debounce(this.handleWidthChange, 100)
時(shí), 其實(shí)都會(huì)返回一個(gè)新的函數(shù),導(dǎo)致addEventListener
和 removeEventListener
方法傳入的回調(diào)函數(shù)已經(jīng)不是同一個(gè)回調(diào)函數(shù),監(jiān)聽(tīng)器沒(méi)有被正確移除,內(nèi)存泄漏。
下面來(lái)看修改后的代碼:
// 版本二 data() { return { debounceWidthChange: null } }, mounted() { this.debounceWidthChange = debounce(this.handleWidthChange, 100) window.addEventListener('resize', this.debounceWidthChange) }, beforeDestroyed() { window.removeEventListener('resize', this.debounceWidthChange) }
修改后,監(jiān)聽(tīng)和移除監(jiān)聽(tīng)的已經(jīng)是同一個(gè)回調(diào)函數(shù)了,看起來(lái)似乎已經(jīng)沒(méi)問(wèn)題。然而,這段代碼還是有內(nèi)存泄漏的問(wèn)題。沒(méi)看出問(wèn)題的小伙伴可以對(duì)比一下正確答案:
// 版本三 data() { return { debounceWidthChange: null } }, mounted() { this.debounceWidthChange = debounce(this.handleWidthChange, 100) window.addEventListener('resize', this.debounceWidthChange) }, beforeDestroy() { window.removeEventListener('resize', this.debounceWidthChange) }
是的,答案非常狗血:Vue只有destroyed
和beforeDestroy
這兩個(gè)生命周期,沒(méi)有 beforeDestroyed
,所以上面的beforeDestroyed
函數(shù)永遠(yuǎn)不會(huì)執(zhí)行,導(dǎo)致了內(nèi)存泄漏。
感謝各位的閱讀,以上就是“怎么排查Javascript內(nèi)存泄漏”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)怎么排查Javascript內(nèi)存泄漏這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。