JS記憶體洩漏例項解析

alex2wong發表於2018-02-23

今天突然想到一個問題,let的塊級作用域,以及閉包的變數引用功能很有意思(這腦洞咋聯想到一起的,囧)。。閉包的使用會影響瀏覽器的GC過程。那麼:

  • JS 物件什麼時候會被自動回收
  • 如何使用正確使用閉包,並避免記憶體洩漏?

JS記憶體洩漏例項解析

先看一個經典例子,迴圈非同步列印問題(沒耐心的直接跳最後一個例項(^▽^))

// 想非同步列印1到5
for(var i=1; i<=5;i++) {
    setTimeout(function(){
        console.log("print: " + i);
    }, i*1000)
}
// 結果
print: 6
print: 6
print: 6
print: 6
print: 6
複製程式碼

由於是非同步呼叫列印函式,所以等呼叫這個函式時,迴圈已經結束,i變成了6,所以連著列印5個6。

第二種情況,如果用let 來宣告i,let 和var 相比至少有如下特性:

  • let宣告的變數擁有塊級作用域
  • 形如for (let x...)的迴圈在每次迭代時都為x建立新的繫結(深度複製
// 1到5
for(let i=1; i<=5;i++) {
    setTimeout(function(){
        console.log("print: " + i);
    }, i*1000)
}
// 結果
print: 1
print: 2
print: 3
print: 4
print: 5
複製程式碼

這種情況下直接通過let, 實際上給每一次回撥函式的註冊,建立了一個閉包,所以列印正常。

第三種情況,通過手動建立閉包也可以實現類似效果。每次迴圈內,返回一個函式引用當時的變數 i,這樣實際上是重新分配了記憶體來儲存i 的值,而不是單純的引用記憶體地址。 尼瑪記憶體蹭蹭往上漲,不過這麼點資料完全不用擔心

// 1到5
for(var i=1; i<=5;i++) {
    setTimeout((function(){
        var b = i; //install timer的時候引用 i 並且return 一個函式
        return function(){
            console.log("print: " + b);
        }
    })(), i*1000)
}
複製程式碼

這個例子很好地說明了閉包對內部變數記憶體地址的保留作用(迴圈1w次就深度複製了1w份i )。但閉包和全域性變數的不當使用可能會導致記憶體洩漏,記憶體居高不下甚至標籤頁直接掛掉。

JS 變數在瀏覽器記憶體中是否被GC 回收要看這個變數所在作用域的生命週期和變數是否被別人引用:

  • 如果是函式內部宣告的變數,並且沒有任何外部變數引用,則函式執行完就銷燬。如果有引用,則該內部變數會一直遊離於記憶體中

JS 物件(引用型別)是儲存在記憶體堆heap中,可以通過Chrome Debug Tool的 Profile 工具生成Heap SnapShot 來檢視。

最後看一個活生生的例項,不出意外分分鐘記憶體佔用1G

function Test()  
{  
    this.obj= {};
    this.index = 1;
    this.timer = null;
    var cache = []; // 內部變數,記憶體隱患...
    this.timer = window.setInterval(() =>{
        this.index += 1; 
        this.obj = {
            val: '_timerxxxxxbbbbxx_' + this.index,
            junk: [...cache]
        };
        cache.push(this.obj);
    }, 1);  
    console.warn("create Test instance..");
}  
test = new Test(); // JS物件開啟定時器不斷分配記憶體
複製程式碼

囉嗦幾句,這個例子的關鍵在於內部變數cache被外部的非同步函式(定時器)引用。 如果不清除定時器,只是把Test類的例項手動設為null,也無濟於事,cache還會繼續佔用記憶體。

在清除定時器,並且把Test類的例項設為null後才成功回收垃圾

Test.prototype.destroy = function(){
    clearInterval(this.timer);
}
function d() {
    // 取消定時器並銷燬Test 例項
    test.destroy();
    test = null;
    console.warn("destroyed test instance..");
}
複製程式碼

清除內部變數cache的引用後,記憶體堆大小立刻下降了40MB.

總結:

  • 函式內部不用的區域性變數及時清理,在清理時要考慮ta的所有引用函式。
  • 非得引用區域性變數,請用非匿名函式,否則難以銷燬引用。

個人見解,說得不對之處歡迎評論指正 : )

參考文章:

es6-in-depth-let-and-const

談一談Javascript記憶體釋放那點事

JS記憶體管理

相關文章