[Ext JS 7]7.6 內存泄露及處理

內存泄漏概念

內存泄漏的粗略定義是指執行一部分代碼之後,內存無限制的增長。精確的可以從以下角度看:

  1. 重複直到耗盡
    假設有一臺具有64Gb可用內存的開發機。 一段代碼運行5次。每次運行後,內存使用量每次都會增加1Mb,並且永遠不會回收。因爲該程序僅使用一小部分可用內存,這種場景還不能說明什麼。 如果代碼部分重複了50,000次,但仍然沒有回收任何內存,那麼就要考慮是否出現內存泄漏了。所以,要判斷是否內存泄漏, 需要對基礎系統施加足夠的壓力,以便觸發其能強制回收內存。
  2. 使用量無限增長
    並不是說運行一段程序,出現了內存的增長,而且調用清除方法依然不釋放就叫內存泄漏, 因爲出於性能考慮,某些程序是會緩存一定的內容在內存中。
    以Ext JS爲例,調用destroy或其他清除操作可能無法釋放所有分配的資源。例如,Ext.ComponentQuery類用於基於字符串選擇器搜索組件。 在內部,此字符串選擇器轉換爲可以在候選組件上執行的函數。 構造此函數的開銷很大,並且通常同一查詢會多次運行。 由於這種重複使用,所生成的函數將保留在內存中。 這裏的關鍵點是緩存機制是有界的。
    緩存的清理最常使用LRU(最近最少使用)算法。但內存消耗過大時,將從緩存中逐出最近最少使用的項目。 一旦達到最大限制,緩存將正常化。 適當保留在內存中是沒有問題的,只有無限地保留資源時,這才成爲問題。

在使用語言和框架進行開發時,有時候需要調用對應的內存清理API,

  • Java語言內置JVM,雖然可以做到自動清理內存,但對於開銷大的對象,也是可以進行手動釋放的。
  • 在C#中,推薦的模式是繼承IDisposable接口。
    無論使用哪種平臺,都必須遵循一些約定,以允許平臺釋放分配的資源。 如果不遵循清理步驟,將導致內存泄漏,因爲無法自動推斷何時不再需要資源。
    Ext JS框架中,提供了destroy()方法用於銷燬不需要使用的組件或對象,這個方法的作用通常是清除DOM元素並取消綁定監聽器。

Ext JS的內存管理

Ext JS開發中的內存管理與實際的內存管理相去甚遠。 更糟糕的是,諸如Window Task Manager或Mac Activity Monitor之類的工具無法提供Ext JS對內存消耗的準確描述。 爲了更好地理解Ext JS內存相關的問題,就需要知道其內存管理的層次。

內存分配層次

  • 首先,開發人員從框架請求資源(例如,創建組件)。
  • 接着,框架從JavaScript引擎請求資源(通常使用運算符new或createElement等)。
  • 隨後,JavaScript引擎從底層進程內存管理器請求資源(通常是C ++內存分配)。
  • 最後,基礎內存管理器從操作系統請求資源。 這是我們可以在任務管理器和活動監視器中觀察到的內存增長。

內存清除層次

  • 開發人員在Ext JS組件或其他資源上調用destroy。
  • Ext JS組件的destroy方法調用其他清除方法,將各種內部引用設置爲null等。
  • JavaScript垃圾收集器稍後決定何時清除堆並回收內存。這通常會推遲到請求新的內存並且可用內存“不足”爲止。內存管理器可能只是決定再次增加堆而不是收集垃圾,因爲增加堆通常更便宜,尤其是在應用程序生命週期的早期。
  • 一旦JavaScript內存管理器決定收集垃圾,就必須確定回收的內存是否應保留爲空閒內存以供將來使用或返回給基礎進程堆。
    取決於JavaScript內存管理器(通常是C ++內存管理器)使用的基礎內存管理器,可用內存可以保留供該進程將來使用或返回給操作系統。只有在到達這一點時,我們才能在任務管理器/活動監視器中看到任何更新。

鑑於以上所述,很明顯,JavaScript開發人員在內存管理方面對全局沒有什麼控制權。 最終,使用常見的OS監視工具檢查內存使用情況並觀察到內存增加(不一定表示“內存泄漏”)

偵測泄漏

應用層級泄漏

當應用程序無法清除框架資源時,這可能導致對象在框架維護的多個集合中累積。 儘管其中的確切詳細信息是特定於版本的,但仍需要檢查以下地方:

  • Ext.ComponentManager
  • Ext.data.StoreManager

框架層級泄漏

儘管已盡一切努力清除框架內部的資源,但總會有出錯的餘地。 從歷史上看,最常見的問題來自泄漏DOM元素。 如果懷疑是這種情況,則sIEve工具可在Internet Explorer中提供出色的泄漏檢測功能。

注意:強烈建議在查看此較低級別的泄漏之前,先解決所有應用程序級別的泄漏。

常見代碼泄漏模式和解決方案

遺漏基類清除

清理派生類中的資源,可能會遺漏對基類的清理。比如:

Ext.define('Foo.bar.CustomButton', {
    extend: 'Ext.button.Button',
    onDestroy: function () {
        // 內存清除方法
    }
});

解決方式: 在子類的onDestroy()方法中記得調用callParent() 清除父類的資源。

沒有刪除DOM監聽

將事件附加到頁面元素之後, 又通過innerHTML覆蓋了頁面元素。 這樣的話,該事件處理程序任然保留在內存中,示例代碼如下:

Ext.fly(someElement).on('click', doSomething);
someElement.parentNode.innerHTML = '';

解決方法:保留對重要元素的引用,並在不再需要它們時調用它們的destroy方法。

保留了對象的引用

對象被刪除,但是對象的引用還存在
比如:定義一個類,創建該類的實例並銷燬:

Ext.define('MyClass', {
    constructor: function() {
        this.foo = new SomeLargeObject();
    },
    destroy: function() {
        this.foo.destroy();
    }
});
this.o = new MyClass();
o.destroy();

以上雖然調用了銷燬方法,但依然會保留o的引用。

解決方法:
將引用設置爲null以確保可以回收內存。 在這種情況下,destroy中的this.foo = null以及調用destroy之後的this.o = null。

保留了閉包的引用

閉包包含對大型對象的引用,而該對象仍在被引用時無法回收。

function runAsync(val) {
    var o = new SomeLargeObject();
    var x = 42;

    return function() {
        return x;  // o is in closure scope but not needed
    }
}
var f = runAsync(1);

上面的情況經常發生是因爲大對象存在於外部作用域中,而內部函數則不需要。 這些事情很容易遺漏,會對內存使用產生負面影響。

解決方法:
使用Ext.Function.bind()或標準JavaScript Function綁定爲此類函數外部聲明的函數創建安全的閉包。

function fn (x) {
    return x;
}
function runAsync(val) {
    var o = new SomeLargeObject();
    var x = 42;
    // other things
    return Ext.Function.bind(fn, null, [x]); // o is not captured
}
var f = runAsync(1);

持續創建有負面效果的實例

比如DOM元素。如果創建沒有銷燬,就會發生內存泄漏,示例代碼如下:

{
    xtype: 'treepanel',
    listeners: {
        itemclick: function(view, record, item, index, e) {

            // Always creating and rendering a new menu
            new Ext.menu.Menu({
                items: [record.get('name')]
            }).showAt(e.getXY());
        }
    }
}

解決方法: 捕獲menu的引用,不需要的時候調用銷燬方法。

清除緩存中的所有註冊

刪除對對象的所有引用很重要。 僅將本地引用設置爲null是不夠的。 如果某些全局單例緩存正在保存引用,則該引用將在應用程序的生存期內保存。

var o = new SomeLargeObject();
someCache.register(o);

// Destroy and null the reference. someCache still has a reference
o.destroy();
o = null;

解決方法:
除了調用destroy之外,請確保從添加了該對象的所有緩存中刪除對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章