本篇文章給大家?guī)砹岁P于JavaScript中內(nèi)存泄露的相關知識,其中包括內(nèi)存泄露是什么,那些情況會引起內(nèi)存泄露等相關問題,希望對大家有幫助。
js 內(nèi)存泄漏
什么是內(nèi)存泄漏?
程序的運行需要內(nèi)存。只要程序提出要求,操作系統(tǒng)或者運行時(runtime)就必須供給內(nèi)存。
對于持續(xù)運行的服務進程(daemon),必須及時釋放不再用到的內(nèi)存。否則,內(nèi)存占用越來越高,輕則影響系統(tǒng)性能,重則導致進程崩潰。
不再用到的內(nèi)存,沒有及時釋放,就叫做內(nèi)存泄漏(memory leak)。
有些語言(比如 C 語言)必須手動釋放內(nèi)存,程序員負責內(nèi)存管理。
char * buffer;buffer = (char*) malloc(42);// Do something with bufferfree(buffer);
上面是 C 語言代碼,malloc方法用來申請內(nèi)存,使用完畢之后,必須自己用free方法釋放內(nèi)存。
這很麻煩,所以大多數(shù)語言提供自動內(nèi)存管理,減輕程序員的負擔,這被稱為"垃圾回收機制"(garbage collector)。
雖然前端有垃圾回收機制,但當某塊無用的內(nèi)存,卻無法被垃圾回收機制認為是垃圾時,也就發(fā)生內(nèi)存泄漏了。
哪些情況會引起內(nèi)存泄漏
1. 意外的全局變量
全局變量的生命周期最長,直到頁面關閉前,它都存活著,所以全局變量上的內(nèi)存一直都不會被回收。
當全局變量使用不當,沒有及時回收(手動賦值 null),或者拼寫錯誤等將某個變量掛載到全局變量時,也就發(fā)生內(nèi)存泄漏了。
2. 遺忘的定時器
setTimeout 和 setInterval 是由瀏覽器專門線程來維護它的生命周期,所以當在某個頁面使用了定時器,當該頁面銷毀時,沒有手動去釋放清理這些定時器的話,那么這些定時器還是存活著的。
也就是說,定時器的生命周期并不掛靠在頁面上,所以當在當前頁面的 js 里通過定時器注冊了某個回調(diào)函數(shù),而該回調(diào)函數(shù)內(nèi)又持有當前頁面某個變量或某些 DOM 元素時,就會導致即使頁面銷毀了,由于定時器持有該頁面部分引用而造成頁面無法正常被回收,從而導致內(nèi)存泄漏了。
如果此時再次打開同個頁面,內(nèi)存中其實是有雙份頁面數(shù)據(jù)的,如果多次關閉、打開,那么內(nèi)存泄漏會越來越嚴重。而且這種場景很容易出現(xiàn),因為使用定時器的人很容易遺忘清除。
3. 使用不當?shù)拈]包
函數(shù)本身會持有它定義時所在的詞法環(huán)境的引用,但通常情況下,使用完函數(shù)后,該函數(shù)所申請的內(nèi)存都會被回收了。
但當函數(shù)內(nèi)再返回一個函數(shù)時,由于返回的函數(shù)持有外部函數(shù)的詞法環(huán)境,而返回的函數(shù)又被其他生命周期東西所持有,導致外部函數(shù)雖然執(zhí)行完了,但內(nèi)存卻無法被回收。
4. 遺漏的 DOM 元素
DOM 元素的生命周期正常是取決于是否掛載在 DOM 樹上,當從 DOM 樹上移除時,也就可以被銷毀回收了
但如果某個 DOM 元素,在 js 中也持有它的引用時,那么它的生命周期就由 js 和是否在 DOM 樹上兩者決定了,記得移除時,兩個地方都需要去清理才能正?;厥账?。
5. 網(wǎng)絡回調(diào)
某些場景中,在某個頁面發(fā)起網(wǎng)絡請求,并注冊一個回調(diào),且回調(diào)函數(shù)內(nèi)持有該頁面某些內(nèi)容,那么,當該頁面銷毀時,應該注銷網(wǎng)絡的回調(diào),否則,因為網(wǎng)絡持有頁面部分內(nèi)容,也會導致頁面部分內(nèi)容無法被回收。
如何監(jiān)控內(nèi)存泄漏
內(nèi)存泄漏是可以分成兩類的,一種是比較嚴重的,泄漏的就一直回收不回來了,另一種嚴重程度稍微輕點,就是沒有及時清理導致的內(nèi)存泄漏
,一段時間后還是可以被清理掉。
不管哪一種,利用開發(fā)者工具抓到的內(nèi)存圖,應該都會看到一段時間內(nèi),內(nèi)存占用不斷的直線式下降,這是因為不斷發(fā)生 GC,也就是垃圾回收導致的。
內(nèi)存不足會造成不斷 GC,而 GC 時是會阻塞主線程
的,所以會影響到頁面性能,造成卡頓,所以內(nèi)存泄漏問題還是需要關注的。
場景一:在某個函數(shù)內(nèi)申請一塊內(nèi)存,然后該函數(shù)在短時間內(nèi)不斷被調(diào)用
// 點擊按鈕,就執(zhí)行一次函數(shù),申請一塊內(nèi)存startBtn.addEventListener("click", function() { var a = new Array(100000).fill(1); var b = new Array(20000).fill(1);});
一個頁面能夠使用的內(nèi)存是有限的,當內(nèi)存不足時,就會觸發(fā)垃圾回收機制去回收沒用的內(nèi)存。
而在函數(shù)內(nèi)部使用的變量都是局部變量,函數(shù)執(zhí)行完畢,這塊內(nèi)存就沒用可以被回收了。
所以當我們短時間內(nèi)不斷調(diào)用該函數(shù)時,可以發(fā)現(xiàn),函數(shù)執(zhí)行時,發(fā)現(xiàn)內(nèi)存不足,垃圾回收機制工作,回收上一個函數(shù)申請的內(nèi)存,因為上個函數(shù)已經(jīng)執(zhí)行結束了,內(nèi)存無用可被回收了。
所以圖中呈現(xiàn)內(nèi)存使用量的圖表就是一條橫線過去,中間出現(xiàn)多處豎線,其實就是表示內(nèi)存清空,再申請,清空再申請,每個豎線的位置就是垃圾回收機制工作以及函數(shù)執(zhí)行又申請的時機。
場景二:在某個函數(shù)內(nèi)申請一塊內(nèi)存,然后該函數(shù)在短時間內(nèi)不斷被調(diào)用,但每次申請的內(nèi)存,有一部分被外部持有。
// 點擊按鈕,就執(zhí)行一次函數(shù),申請一塊內(nèi)存var arr = [];startBtn.addEventListener("click", function() { var a = new Array(100000).fill(1); var b = new Array(20000).fill(1); arr.push(b);});
看一下跟第一張圖片有什么區(qū)別?
不再是一條橫線了吧,而且橫線中的每個豎線的底部也不是同一水平了吧。
其實這就是內(nèi)存泄漏
了。
我們在函數(shù)內(nèi)申請了兩個數(shù)組內(nèi)存,但其中有個數(shù)組卻被外部持有,那么,即使每次函數(shù)執(zhí)行完,這部分被外部持有的數(shù)組內(nèi)存也依舊回收不了,所以每次只能回收一部分內(nèi)存。
這樣一來,當函數(shù)調(diào)用次數(shù)增多時,沒法回收的內(nèi)存就越多,內(nèi)存泄漏的也就越多,導致內(nèi)存使用量一直在增長
另外,也可以使用 performance monitor
工具,在開發(fā)者工具里找到