javascript内存泄露

先说说什么是内存泄露,在一个进程中,如果某一块内存无法访问,且直到进程结束为止也无法释放,那么就发生了内存泄露。通常这种情况发生在C++之类的手 动管理内存的语言编写的程序中,程序员忘记delete或者free会导致内存泄露。本文主要讨论的是浏览器中的内存泄露问题,也就是说,javas

cript程序导致的内存泄露。

        目前为止最权威的关于浏览器内存泄露的文章应该是以下2篇 分别来自微软的Understanding and Solving Internet Explorer Leak Patterns(中文版)和来自IBM的Memory leak patterns in JavaScript(中文版)

       但是这2篇对js语言的认识不够深入,所以讨论的内存泄露问题和解决方案都存在一些偏差。更重要的是,他们太老了,没有介绍IE7的内存泄露新模式。希望本文下面的部分能让读到此文的人更加清晰的认识内存泄露问题。

1.javascript对象的基础知识

(1)创建对象

js中创建对象的方式非常自由,通常有这样几种:直接量、new表达式、内置函数、函数调用。后面是几个例子:

直接量:{"a":10,"b":30}

new表达式:var o=new cls();

内置函数:var e=document.createElement("div");var a=new ActiveXObject("XML2.0.XMLHTTP");

函数调用:function f(){};f();

(2)特殊的对象——作用域对象

  值得一提的是函数调用也会创建对象

function f(){ 

   var a=10; 

   var b=20; 

  }

复制代码

尽管按照语言标准无法以任何方式访问,但是f函数在每次执行时都会创建一个有属性a和b的对 象,这被称为作用域对象。而js将维护一个被称为scopechain的链表,它是一条由当前可访问的所有作用域对象组成的链表。因为js的作用域规则是 定义时的作用域,所以每个函数对象被创建时都会以一个属性[[scope]]保存它的外部作用域链。

特别地,在FireFox中,允许用__parent__访问[[scope]]属性所属的函数

  关于js的更多,可以去查阅js标准文档ECMA262(HTML版),这里无法完整地介绍js的对象机制。

2.内存泄露的原因

作为一门垃圾回收的语言(注意不是垃圾语言),内存泄露的原因只有一个:引擎的bug(本小节用于休闲、调节气氛)

3.内存泄露的方式

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

  • 循环引用
  • 自动类型装箱转换
  • 某些DOM操作

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

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

什么是循环引用?首先搞清楚什么是引用,一个对象A的属性被赋值为另一个对象B时,则可以称A 引用了B。假如B也引用了A,那么A和B之间构成了循环引用。同样道理 如果能找到A引用B B引用CC又引用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方法。

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

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

4.内存泄露的解决方案

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

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

to be continued......

coming soon:

  • 显式类型转换
  • 避免事件导致的循环引用
  • 不影响返回值地打破循环引用
  • 延迟appendChild
  • 代理DOM对象
  • 4.内存泄露的解决方案
    • 显式类型转换
    首先说说最容易处理的情况 对于类型转换造成的错误,我们可以通过显式类型转换来避免:
    var s=newString("lalalalala");//此处将string转换成object 

    alert(s.length);

    DE>
    复制代码
    这个太容易了,算不上正经方案。不过类型转换泄露也就这一种处理方法了。
    • 避免事件导致的循环引用
    在比较成熟的js程序员里,把事件函数写成闭包是再正常不过了:
    function A(){ 

        var 
    a=document.createElement("div"); 

        
    a.onclick=function(){ 

            
    alert("hi"); 

        } 

    }


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

        var 
    a=document.createElement("div"); 

        var 
    b=document.createElement("div"); 

        
    a.onclick=function(){ 

            
    alert(b.outerHTML); 

        } 

        return 
    a

    }


    DE>
    复制代码
    如何将它的逻辑表达出来 还避免内存泄露?分析一下这个内存泄露的形式:只要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); 

        } 

    }


    DE>
    复制代码
    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

        } 

    }


    DE>
    复制代码
    finally在try之后执行,如果finall块不返回值,才会返回try块的返回值。
    • 延迟appendChild
    还记得函数的lazyinitalize吧,对于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); 

    }


    DE>
    复制代码
    • 垃圾箱
    对于ie7,我比较无可奈何,因为DOM对象不会被CG程序回收,只有离开页面时会被回收,所以我的建议是:使用DOM要有节制,尽量多用innerHTML吧...... good luck.
    一旦你使用了DOM对象,千万不要试图o=null,你可以设置一个叫做Garbage的div并且将其display设置为none,将不用的DOM对象存入其中(就是appendChild上去)就好了
    • 代理对象
    这是Ext的做法,这里只是顺带提一下。将每个元素用一个"代理对象"操作,不论appendChild还是其他操作都不是对DOM对象本身的操作,而是 通过这个代理对象操作。这是一个很不错的Proxy模式,不过要想避免泄露还是需要一点功夫的,并非用了Proxy之后就不会泄露,有时反而更容易泄露。
    5 .FAQ
    • 内存泄露是内存占用很大么? 不是,即使1byte内存也叫做内存泄露。
    • 程序中提示,内存不足,是内存泄露么?不是,这一般是无限递归函数调用导致栈内存溢出。
    • 内存泄露是哪个区域泄露?堆区,栈区是不会泄露的。
    • window对象是DOM对象么?不是,window对象参与的循环引用不会内存泄露。
    • 内存泄露后果是什么?大多数时候后果不很严重,但过多DOM操作会导致网页执行变慢。
    • 跳转页面后,内存泄露仍然存在么?仍然存在,直到关闭浏览器。
    • FireFox也会内存泄露么?FF2仍然有内存泄露
原文地址:https://www.cnblogs.com/firstdream/p/2383490.html