JavaScript之記憶體洩漏【四】

jinlong_zhang發表於2018-04-22

接著上文閉包,我們來聊聊記憶體洩漏,看完下面文章,你將瞭解到:

1、什麼是記憶體洩漏
2、有哪些code會導致記憶體洩漏
3、如何規避記憶體洩漏
複製程式碼

一、探究記憶體洩漏之前,我們先了解下垃圾回收機制(GC)

垃圾回收機制(GC):

由於字串、物件和陣列沒有固定大小,所有當他們的大小已知時,才能對他們進行動態的儲存分配。
JavaScript程式每次建立字串、陣列或物件時,直譯器都必須分配記憶體來儲存那個實體。只要
像這樣動態地分配了記憶體,最終都要釋放這些記憶體以便他們能夠被再用,否則,JavaScript的解
釋器將會消耗完系統中所有可用的記憶體,造成系統崩潰。--《JavaScript權威指南(第四版)》
複製程式碼

對以上簡單的總結就是:垃圾收集器會定期(週期性)找出那些不在繼續使用的變數,然後釋放其記憶體。下面就讓我們一起看看垃圾收集器是如何回收記憶體的:

垃圾回收機制(gc)分為:標記-清除演算法引用計數垃圾收集

1、標記-清除演算法最為常用 在JavaScript中,標記清除是最常用的方式. 例如:在函式中宣告一個變數,就將這個變數定義為“進入環境”。從邏輯上講 進入環境的變數其記憶體不能被釋放,因為只要有流執行,這些變數都可能被用到。當變數離開環境後,直譯器標記其為“離開環境”,此時,當垃圾收集器,在下個週期時就會回收這部分的記憶體.

function foo(){
    let num=1;//被標記,進入環境
    let count=60;//被標記,進入環境
}

foo();//函式執行完畢,num,count被標記為離開環境,被gc回收
複製程式碼

2、引用計數垃圾收集

JavaScript之記憶體洩漏【四】
這個演算法把“物件是否不再需要”簡化定義為“物件是否可以獲得”

這個演算法假定設定一個叫做根(root)的物件(在Javascript裡,根是全域性物件)。定期的,垃圾回收器將從根開始,找所有從根開始引用的物件,然後找這些物件引用的物件……從根開始,垃圾回收器將找到所有可以獲得的物件和所有不能獲得的物件。可以獲得的物件,將不會被垃圾回收器回收,不能獲得的物件將會被垃圾回收器回收,從而達到釋放記憶體的目的. 具體實現:

function obj(){
    let a={};//定義a引用物件 初始化 a引用次數為0
    let b=a;//a被引用次數1
    let c=a;//a被引用次數2
    let b={};//因b引用被切斷,此時上下文a的引用次數減一,引用次數變為1
    let c=[];//同上此時a的引用次數為0,記憶體等待回收
}
複製程式碼

看了上面的例子,我們再看下理論:

直譯器通過記錄每個值被引用的次數,來判定該值是否不再需要.

當宣告瞭一個變數(這裡為b)並將一個引用型別(function,object,array)值(這裡為a),賦值給該變數(b)時,則這個值(a)的引用次數就是1.當這個變數又被賦值給另外一個變數時,此時這個值(a)的引用計數加1,一次類推.但當某一個變數,又重新被賦值別的變數(比如b,原來是引用a此時,被賦值一個物件),則原引用型別的值(a),引用此時減一,以此類推...

如上code:a的引用計數為0,物件不再被使用,等待垃圾收集器回收. 理論上,如果按照以上的原理進行coding,記憶體是能夠及時被回收掉,有效的節省記憶體資源,但往往事與願違,下面讓我們一探記憶體洩漏

二、記憶體洩漏

記憶體洩漏(Memory Leak)顧名思義.就是指程式中已動態分配的堆記憶體由於某種原因程式未釋放或無法釋放【gc無法進行回收,比如因:閉包、全域性變數的引用、迴圈引用、子dom的引用、以及事件、定時沒有被及時的釋放等都會引起記憶體洩漏】,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果的現象。

下面我們來看看,有哪些方面的記憶體洩漏:

1、首當其衝的是閉包引起的記憶體洩漏,最為常見

閉包可以維持函式內部變數駐留記憶體,使其得不到釋放.

function onclick(){
    let obj=document.getElementById("button");
    obj.onclick=function(){
        //.
    }
}
複製程式碼

上例,函式內部定義了一個變數,這個變數的事件引用了內部函式,並且這個事件回撥函式的引用外暴了,形成閉包.

解決方法一: 將事件處理函式定義在函式外部,解除閉包.

//defeat out
function onClickHandeler(){
    //to do something
}
function onclick(){
    let obj=document.getElementById("button");
    obj.onclick=onClickHandeler;
}
複製程式碼

解決方法二: 刪除對dom的引用,解除繫結

function(){
    let obj=document.getElementById("button");
    obj.onclick=function onClickHandeler(){
    //to do something
    }
    obj = null;
}
複製程式碼

2、意外的全域性變數引起的記憶體洩漏

function(){
    menu='navite';//menu為一個全域性變數,全域性變數常駐記憶體
}
複製程式碼

3、沒有清理的dom元素的引用

//dom still exist
function click(){
    const button=document.getElementById('button');
    butto.click();
}
// button has removed
function removeButton(){
    document.body.removeChild(document.getElementById('button'));
}
複製程式碼

4、定時器沒有被及時的被銷燬

var conentByName=getConentByName();
setInterval(function(){
    let content=document.getElemetById('key_id');
    if(content){
        content.text = conentByName;
    }
},1000);
複製程式碼

如果key_id這個元素從dom元素移除,那麼這個定時仍然存在.因為函式中包含對conentByName的引用,外部物件也得不到釋放. 解決辦法:在呼叫之前,對定時進行清除.

clearInterval();
var conentByName=getConentByName();
setInterval(function(){
    let content=document.getElemetById('key_id');
    if(!!content){
        content.text = conentByName;
        return;
    }
    clearInterval();
    conentByName=null;
},1000);
複製程式碼

5、子元素存在引起的記憶體洩漏

<div id='parent'>
<div>
<ul>
<li><li>
<li><li>
<li><a class='child'></a><li>
</ul>
</div>
</div>
複製程式碼
function(){
    let parentById=document.getElementById('parent');
    let childByName=document.getElementByName('child');
    document.body.removeChild(parentById);
}
複製程式碼

parentById,childByName直接被JavaScript變數引用,中間dom鏈被間接引用.所以 雖然,parentById層dom元素被刪除,但由於childByName被間接引用,導致childByName這條dom鏈仍然存在.都不會被刪除.

解決方案: 把不需要的子元素進行遍歷刪除

6、監聽事件造成的洩漏

this.filterWidget = new FilterWidget(this, {
        categoryList: [{
            title: '0',
            startIndex: 0,
            list: ['zjl','lisi']
        }]
    });
this.filterWidget.on('EVENT_FILTER',  this);
複製程式碼

上面事件,如果不及時off掉,會造成事件監聽的重複註冊.

解決辦法:呼叫之前,off掉並且在頁面離開時,在生命週期鉤子中銷燬掉 this.filterWidget.off('EVENT_FILTER', this);

7、對Promise物件,在不用的時候及時的reject掉

如下:

define(function (require) {
    'use strict';
    var View = require('core/View'),
    return View.extend({
        template: templates['page'],
        onInit: function (option) {
            this.providerPromise = this.Provider().done(function (html) {
                    // to do html
                }).fail(function(){
                }); 
        },
        onDestroy:function(){
             this.providerPromise && this.providerPromise.reject();
             this.filterWidget.on('EVENT_FILTER',  this);
        });
});
複製程式碼

8、迴圈引用造成的洩漏,只有老的IE版本才有,目前ie9及以上已經優化了這個bug,這裡就不做探討了.

三、規避記憶體洩漏

1、謹慎使用閉包
a、在業務不需要用到的內部函式,可以重構在函式外,實現解除閉包.
b、閉包內,區域性變數使用後或不再需要,及時的清除掉
2、減少不必要的全域性變數,如果用了,最好在宣告週期鉤子中或再函式呼叫之前,及時的清除掉.
3、減少生命週期較長的物件,及時對無用的資料進行釋放銷燬.
4、避免建立過多的物件,對不用的物件及時的釋放.
5、對註冊的事件,再不用的時候,及時的解耦.釋放資源.
複製程式碼

JavaScript之Promise

相關文章