`
rock
  • 浏览: 56607 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

【转】JavaScript 内存泄露

阅读更多

Javascript的内存泄漏,不是太可怕。它只会悄悄的,慢慢的把你的浏览器拖的巨慢无比,让你愤怒的拍案而起,大骂微软出品的破烂浏览器危害社会。这一切有可能并不是浏览器的错,可能只是因为网页 上有些javascript 的内存泄漏罢了。
在科技日益发达今天,我们有必要武装自己,以及自己的浏览器,这样万一浏览器倒下了,还能知道到底是死在谁家的网页上面。下面这个Firefox插件是推荐给那些UI设计者或者开发 人员的:Leak Monitor


This extension pops up an alert dialog to warn chromeand extension developers about one particular type of leak. It warnswhen chrome windows close but leave other code pointing at theirJavaScript objects.


Works with:
         Firefox         1.5 – 3.0         ALL
         Thunderbird         1.5 – 3.0         ALL
在你访问一张网页的时候,如果有javascript内存泄漏,这个extension就会啪的一下给你弹出一张内存泄漏的清单。就拿现在用的 wordpress后台来说吧,Leak Monitor也是毫不客气的给弹了一个窗口,主要是因为使用了大名鼎鼎的prototype.js…下载
https://addons.mozilla.org/firefox/2490/

今天下午同事让帮忙看web内存泄露问题。当时定位到创建ActiveX 对象的时候产生的,于是我对这个奇怪的问题进行了一些深入探索。

很多时候我都依赖javascript的垃圾回收机制,所以对C 以及C++ 操作内存语言常发生的内存泄露是很陌生的。当时创建回调函数用了闭包,当然最终的解决方法是也避免闭包调用。

 

    随着这个问题的浮出水面,我回忆起以前的一个项目中也应该存在这个内存泄露问题。于是查阅了相关资料把类似的问题总结下来,希望对大家也有帮助。


原因:对于一门具有垃圾收回机制的语言存在内存泄露,其原因不外乎就是javascript脚本引擎存在bug。


很多时候,我们要做的不是去修正那样的bug,而是想办法去规避。

目前发现的可能导致内存泄露的代码 有三种:

· 循环引用

· 自动类型装箱转换

· 某些DOM操作

下面具体的来说说内存是如何泄露的

循环引用:这种方式存在于IE6和FF2中(FF3未做测试),当出现了一个含有DOM对象的循环引用时,就会发生内存泄露。

什 么是循环引用?首先搞清楚什么是引用,一个对象A的属性被赋值为另一个对象B时,则可以称A引用了B。假如B也引用了A,那么A和B之间构成了循环引用。 同样道理 如果能找到A引用B B引用C C又引用A这样一组饮用关系,那么这三个对象构成了循环引用。当一个对象引用自己时,它自己形成了循环引用。注意,在js中变量永远是对象的属性,它可以 指向对象,但决不是对象本身。

循环引用很常见,而且通常是无害的,但如果循环引用中包含DOM对象或者ActiveX对象,那么就会发生内存泄露。例子:

var a=document.createElement("div");
var b=new Object();
a.b=b;
b.a=a;

很多情况下循环引用不是这样的明显,下面就是著名的闭包(closure)造成内存泄露的例子,每执行一次函数A()都会产生内存泄露。试试看,根据前面讲的scope对象的知识,能不能找出循环引用?

function A()...{
    var a=document.createElement("div");
    a.onclick=function()...{
        alert("hi");
    }
}
A();

OK, 让我们来看看。假设A()执行时创建的作用域对象叫做ScopeA 找到以下引用关系
ScopeA引用DOM对象document.createElement("div");
DOM对象document.createElement("div");引用函数function(){alert("hi")}
函数function(){alert("hi")}引用ScopeA

这样就很清楚了,所谓closure泄露,只不过是几个js特殊对象的循环引用而已。

自动类型装箱转换:这种泄露存在于ie6 ie7中。这是极其匪夷所思的一个bug,看下面代码

var s="lalalalala";
alert(s.length);

这 段代码怎么了?看看吧,"lalalalala"已经泄露了。关键问题出在s.length上,我们知道js的类型中,string并非对象,但可以对它 使用.运算符,为什么呢?因为js的默认类型转换机制,允许js在遇到.运算符时自动将string转换为object型中对应的String对象。而这 个转换成的临时对象100%会泄露(汗一下)。

某些DOM操作也可能导致泄露 这些恶心的bug只存在于ie系列中。在ie7中 因为试图fix循环引用bug而让情况变得更糟,以至于我对写这一段种满了恐惧。

从ie6谈起,下面是微软的例子,

<html>
    <head>
        <script language="JScript">...
        function LeakMemory()
        ...{
            var hostElement = document.getElementById("hostElement");
            // Do it a lot, look at Task Manager for memory response
            for(i = 0; i < 5000; i++)
            ...{
                var parentDiv =
                    document.createElement("<div onClick='foo()'>");
                var childDiv =
                    document.createElement("<div onClick='foo()'>");
                // This will leak a temporary object
                parentDiv.appendChild(childDiv);
                hostElement.appendChild(parentDiv);
                hostElement.removeChild(parentDiv);
                parentDiv.removeChild(childDiv);
                parentDiv = null;
                childDiv = null;
            }
            hostElement = null;
        }

        function CleanMemory()
        ...{
            var hostElement = document.getElementById("hostElement");
            // Do it a lot, look at Task Manager for memory response
            for(i = 0; i < 5000; i++)
            ...{
                var parentDiv =
                    document.createElement("<div onClick='foo()'>");
                var childDiv =
                    document.createElement("<div onClick='foo()'>");
                // Changing the order is important, this won't leak
                hostElement.appendChild(parentDiv);
                parentDiv.appendChild(childDiv);
                hostElement.removeChild(parentDiv);
                parentDiv.removeChild(childDiv);
                parentDiv = null;
                childDiv = null;
            }
            hostElement = null;
        }
        </script>
    </head>
    <body>
        <button>Memory Leaking Insert</button>
        <button>Clean Insert</button>
        <div id="hostElement"></div>
    </body>
</html>

看 看结果吧,LeakMemory造成了内存泄露,而CleanMemory没有,循环引用了么?仔细看看没有。那么是什么问题呢?MS的解释是"插入顺序 不对",必须先将父级元素appendChild。这听起来有些模糊,这里给出一个比较恰当的等价描述:永远不要使用DOM节点树之外元素的 appendChild方法。

我 曾经看到过这样的说法,创建dom的时候,先创建子节点,当子节点完善后一次性添加到页面中,不要一点点朝页面上加东西,尽量减少document刷新次 数,这样效率会高点。(打个比方就是应该像 LeakMemory )可见这里我还是被某些书籍误导了。至少他没有告诉我内存泄露的问题。

接下来是ie7和ie8 beta 1中运行这段程序 , 看到什么?没看错吧,2个都泄露了!别急,刷新一下页面就好了。为什么呢?ie7改变了DOM元素的回收方式:在离开页面时回收DOM树上的所有元素,所 以ie7下的内存管理非常简单:在所有的页面中只要挂在DOM树上的元素,就不会泄露,没挂在DOM树上,肯定泄露。所以,ie7中记住一条原则:在离开 页面之前把所有创建的DOM元素挂到DOM树上。

接 下来谈谈ie7的这个设计吧,坦白的说,这种做法纯粹是偷懒的垃圾做法。动态垃圾回收不是保证所有内存都在离开页面时收回,而是要保证内存的充分利用,运 行时不回收,等到离开时回收有什么用?这只是名义上的避免泄露,其实是完全的泄露。况且还没有回收DOM节点树之外的元素。

4.内存泄露的解决方案

内存泄露怎么办?真的以后不用闭包了么?没法封装控件了?这样做还不如要了js程序员 的命,嘿嘿。

事实上,通过一些很简单的小技巧,可以巧妙的绕开这些危险的bug。

to be continued......

coming soon:

· 显式类型转换

· 避免事件导致的循环引用

· 不影响返回值地打破循环引用

· 延迟appendChild

· 代理DOM对象

· 显式类型转换

首先说说最容易处理的情况 对于类型转换造成的错误,我们可以通过显式类型转换来避免:

var s=newString("lalalalala");//此处将string转换成object
alert(s.length);

这个太容易了,算不上正经方案。不过类型转换泄露也就这一种处理方法了。

· 避免事件导致的循环引用

在比较成熟的js程序员里,把事件函数写成闭包是再正常不过了:

function A(){
    var a=document.createElement("div");
    a.onclick=function(){
        alert("hi");
    }
}

这将导致内存泄露。按照IBM那两位老大的说法,当然是把函数放外面或者a=null就没问题了,不过还要访问A()里面的变量呢?假如有下面的代码:

function A(){
    var a=document.createElement("div");
    var b=document.createElement("div");
    a.onclick=function(){
        alert(b.outerHTML);
    }
    return a;
}

如何将它的逻辑表达出来 还避免内存泄露? 分析一下这个内存泄露的形式:只要onclick的外部环境中不包含a那么,就不会泄露。那么办法有2个一是将环境到a的引用断开 另一个是将function到环境的引用断开,但是,如果要在函数中访问b就不能将Function放到外面,如果要返回a的值,就不能a=null,怎 么办呢?

解决方案1:

构造一个不含a的新环境

function A(){
    var a=document.createElement("div");
    var b=document.createElement("div");
    a.onclick=BuildEvent(b);
    return a;
}

function BuildEvent(b)
{
    return function(){
        alert(b.outerHTML);
    }
}

a本身可以通过this访问,将其它需要访问的外层函数变量传递给BuildEvent就可以了。保持BuildEvent定义和调用的参数名一致,会带来方便。

解决方案2:

在return 之后a=null,不可能? 看看下面:

function A(){
    try{
        var a=document.createElement("div");
        var b=document.createElement("div");
        a.onclick= function(){
            alert(b.outerHTML);
        }
        return a;
    } finally {
        a=null;
    }
}

finally在try之后执行,如果finall块不返回值,才会返回try块的返回值。

· 延迟appendChild

还 记得函数的lazy initalize吧,对于ie恶心至极的DOM操作泄露,我们需要用类似的方法去处理。在一个函数中构造一个复杂对象,在需要的时候将之 appendChild到DOM树上,这是很常见的做法,但在IE6中,这样做将导致所谓的"插入顺序内存泄露",没有别的办法,我们只能用一个数组 parts保存子节点,编写一个appendTo方法先序遍历节点树,去把它挂在某个DOM节点上。

function appendTo(Element)
...{
    Element.appendChild(this);
    if(!this.parts)return;
    for(var i=0;i<this.parts.length;i++)
        parts.appendTo(this);
}

 

· 垃圾箱

对于ie7,我比较无可奈何,因为DOM对象不会被CG程序回收,只有离开页面时会被回收,所以我的建议是:使用DOM要有节制,尽量多用innerHTML吧...... good luck.

一旦你使用了DOM对象,千万不要试图o=null,你可以设置一个叫做Garbage的div并且将其display设置为none,将不用的DOM对象存入其中(就是appendChild上去)就好了

· 代理对象

这 是Ext的做法,这里只是顺带提一下。将每个元素用一个"代理对象"操作,不论appendChild还是其他操作都不是对DOM对象本身的操作,而是通 过这个代理对象操作。这是一个很不错的Proxy模式,不过要想避免泄露还是需要一点功夫的,并非用了Proxy之后就不会泄露,有时反而更容易泄露。

5 .FAQ

1 内存泄露是内存占用很大么? 不是,即使1byte内存也叫做内存泄露。

2 程序中提示,内存不足,是内存泄露么?不是,这一般是无限递归函数调用导致栈内存溢出。

3 内存泄露是哪个区域泄露?堆区,栈区是不会泄露的。

4 window对象是DOM对象么?不是,window对象参与的循环引用不会内存泄露。

5 内存泄露后果是什么?大多数时候后果不很严重,但过多DOM操作会导致网页执行变慢。

6 跳转页面后,内存泄露仍然存在么?仍然存在,直到关闭浏览器。

      7 FireFox也会内存泄露么?FF2仍然有内存泄露

 

其他与js内存泄漏相关的文章:http://www.ibm.com/developerworks/cn/web/wa-memleak/index.html

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics