最近發現項目有個bug,同時運行多個任務的時候,前端頁面報內存不足而導致頁面崩潰,這很明顯就是內存泄露了。我查看了一下,運行的過程中,因為運行時間很久,所以前端和后台約定了,用計時器setInternal定時去請求后台運行狀態,當運行狀態為完成時,前端會清除定時器。我預估是因為計時器而導致的內存泄露,在執行計時器代碼的時候,任務管理器的物理內存消耗一直在增加,這樣的話,要是多個任務同時在執行,而且任務執行較久的話,那樣物理內存就有可能會被占用完。后面我也復現了這個場景,果然是因為計時器的原因。
一、探究過程
疑惑1:會不會是因為JavaScript引擎是單線程,計時器會不斷把事件不斷放入事件隊列,而任務執行時間很長,所以才會導致事件隊列堆滿而導致內存泄露?
資料答疑:首先,糾正大家一個錯誤的理解,定時器並不是嚴格意義上會按個多少秒執行的,它可能會出現執行延遲或提前。在運行本行代碼的時候,將定時器的代碼添加到了事件隊列當中,而不是何時執行/運行代碼。此時需要等到當前“事件處理程序”運行之后再去執行定時器代碼。換句話說,就是,並非是設置的毫秒數后就執行定時器代碼,執行的時間是有可能提前/延后的。同時,JavaScript引擎設置了:僅當隊列中沒有該定時器的任何其他代碼實例時,才能夠將定時器代碼添加到隊列。所以不會出現導致連續運行多次的情況。
疑惑2:那到底是什么導致內存泄露呢?
資料答疑:內聯書寫setInterval時,由於匿名函數被定義於全局中,不能夠計時器的清除,因此很容易造成內存泄露。
二、實驗測試
剛好我的計時器setInternal是用匿名函數寫的,很有可能是因為這個原因,所以我用了匿名函數和命名函數測試了一下。
1、匿名函數
代碼:
1 mounted(){ 2 let self = this; 3 self.setInternalId = setInterval(()=>{ 4 let sum = 0,i=1; 5 while(i<100000000){ 6 sum+=i++; 7 // sum=parseInt(sum/2)
8 } 9 let now = new Date(); 10 console.log(sum,'秒數:',now.getSeconds()); 11 },2000); 12 }, 13 beforeDestroy(){ 14 let self = this
15 clearInterval(self.setInternalId); 16 console.log('消除定時器啦。。。。') 17 },
我發現物理內存是很緩慢增長的,所以要時間夠長才能會有明顯的區別,所以不能確定是匿名函數導致的。
2、命名函數
代碼:
1 mounted(){ 2 let self = this; 3 self.setInternalId = setInterval(this.getSum,2000); 4 }, 5 beforeDestroy(){ 6 let self = this
7 clearInterval(self.setInternalId); 8 console.log('消除定時器啦。。。。') 9 }, 10 methods:{ 11 getSum(){ 12 let sum = 0,i=1; 13 while(i<100000000){ 14 sum+=i++; 15 // sum=parseInt(sum/2)
16 } 17 let now = new Date(); 18 console.log(sum,'秒數:',now.getSeconds()); 19 } 20 }
我持續觀察了半個多小時物理內存的變化,發現內存是時增長時減低,增長或降低的幅度都不會很大。
3、增加http請求
后面我在里面增加一個htttp請求后台數據,發現物理內存是一直上升的,上升的幅度明顯比匿名函數大,上升的速度也很快,所以可能是前端請求導致的內存泄露。同時為了校驗是setInternal還是http請求導致的內存泄露,我做了以下的代碼校驗:
1 mounted(){ 2 let self = this; 3 self.setInternalId2 = setInterval(()=>{ 4 let now = new Date(); 5 console.log('第一個:',now.getSeconds()); 6 self.getAllTasks(1); // http請求
7 self.setInternalId3 = setInterval(()=>{ 8 let sum =0,i=1; 9 while(i<1000000){ 10 sum+=i++; 11 } 12 let now = new Date(); 13 console.log('第二個:',now.getSeconds()); 14 },3000) 15 },1000) 16 }, 17 beforeDestroy(){ 18 let self = this; 19 clearInterval(self.setInternalId2); 20 clearInterval(self.setInternalId3); 21 console.log('消除計時器。。。。。') 22 },
發中間的setInternal計時器刪掉后,物理內存消耗還是持續快速的增長。后面查資料得知,瀏覽器對單窗口的http請求數量是有限制的,谷歌瀏覽器可以並發執行最多6個,所以會導致http請求阻塞,從而消耗內存。
總結:這兩個測試並不能證明是匿名函數所引起的內存泄露,很有可能是不斷重復執行 ajax 引起的,但是我目前還查不到相關的資料證明,這個問題還有待考察。
三、常見的內存泄露
1、全局變量
在瀏覽器的環境下,全局變量對象就是window,定義全局變量如下:
1 function index(){ 2 bar = "dsasd" ; 3 } 4 //上面代碼相當於
5 function index(){ 6 window.bar = "dsasd"; 7 }
如果定義變量的時候忘記加上let或var,這時一個全局變量就會被創建出來,還有另一種定義全局變量
1 function foo(){ 2 this.variable= "potential accidental global"; 3 } 4 // 函數自身發生了調用,this 指向全局對象(window),(這時候會為全局對象 window 添加一個 variable 屬性)而不是 undefined。
5
6 foo();
解決辦法:為了防止這種錯誤的發生,在 JavaScript 文件開頭添加 'use strict';
語句。這個語句實際上開啟了解釋 JavaScript 代碼的嚴格模式,這種模式可以避免創建意外的全局變量。
2、定時器和回調函數
定時器造成內存泄露的主要原因是周期函數一直在運行,處理函數並不會被回收(只有周期函數停止運行之后才開始回收內存)。如果周期處理函數不能被回收,它的依賴程序也同樣無法被回收。這意味着一些資源,也許是一些相當大的數據都也無法被回收。
觀察者即監聽事件,當它們不再被需要的時候(或者關聯對象將要失效的時候)顯式地將他們移除是十分重要的。現在,當觀察者對象失效的時候便會被回收,即便 listener 沒有被明確地移除,絕大多數的瀏覽器可以或者將會支持這個特性。盡管如此,在對象被銷毀之前移除觀察者依然是一個好的實踐。
3、DOM 之外的引用
4、閉包
內存泄露可以詳細看:https://blog.csdn.net/fay462298322/article/details/53172176