[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之外,请确保从添加了该对象的所有缓存中删除对象。

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