zoukankan      html  css  js  c++  java
  • 关于Javascript的内存泄漏问题的整理稿

    常规循环引用内存泄漏和Closure内存泄漏

    要了解javascript的内存泄漏问题,首先要了解的就是javascript的GC原理。

    我记得原来在犀牛书《JavaScript: The Definitive Guide》中看到过,IE使用的GC算法是计数器,因此只碰到循环 引用就会造成memory leakage。后来一直觉得和观察到的现象很不一致,直到看到Eric的文章,才明白犀牛书的说法没有说得很明确,估计该书成文后IE升级过算法吧。在IE 6中,对于javascript object内部,jscript使用的是mark-and-sweep算法,而对于javascript object与外部object(包括native object和vbscript object等等)的引用时,IE 6使用的才是计数器的算法。

    Eric Lippert在http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx一文中提到IE 6中JScript的GC算法使用的是nongeneration mark-and-sweep。对于javascript对算法的实现缺陷,文章如是说:
    "The benefits of this approach are numerous, but the principle benefit is that circular references are not leaked unless the circular reference involves an object not owned by JScript. "
    也就是说,IE 6对于纯粹的Script Objects间的Circular References是可以正确处理的,可惜它处理不了的是JScript与Native Object(例如Dom、ActiveX Object)之间的Circular References。
    所以,当我们出现Native对象(例如Dom、ActiveX Object)与Javascript对象间的循环引用时,内存泄露的问题就出现了。当然,这个bug在IE 7中已经被修复了http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html]。

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp 中有个示意图和简单的例子体现了这个问题:

    < html >
        
    < head >
            
    < script language = " JScript " >

            
    var  myGlobalObject;

            
    function  SetupLeak()  // 产生循环引用,因此会造成内存泄露
            {
                
    //  First set up the script scope to element reference
                myGlobalObject  =
                    document.getElementById(
    " LeakedDiv " );

                
    //  Next set up the element to script scope reference
                document.getElementById( " LeakedDiv " ).expandoProperty  =
                    myGlobalObject;
            }


            
    function  BreakLeak()  // 解开循环引用,解决内存泄露问题
            {
                document.getElementById(
    " LeakedDiv " ).expandoProperty  =
                    
    null ;
            }
            
    </ script >
        
    </ head >

        
    < body onload = " SetupLeak() "  onunload = " BreakLeak() " >
            
    < div id = " LeakedDiv " ></ div >
        
    </ body >
    </ html >
       上面这个例子,看似很简单就能够解决内存泄露的问题。可惜的是,当我们的代码中的结构复杂了以后,造成循环引用的原因开始变得多样,我们就没法那么容易观察到了,这时候,我们必须对代码进行仔细的检查。

    尤其是当碰到Closure,当我们往Native对象(例如Dom对象、ActiveX Object)上绑定事件响应代码时,一个不小心,我们就会制造出Closure Memory Leak。其关键原因,其实和前者是一样的,也是一个跨javascript object和native object的循环引用。只是代码更为隐蔽,这个隐蔽性,是由于javascript的语言特性造成的。但在使用类似内嵌函数的时候,内嵌的函数有拥有一个reference指向外部函数的scope,包括外部函数的参数,因此也就很容易造成一个很隐蔽的循环引用,例如:
    DOM_Node.onevent ->function_object.[ [ scope ] ] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp]有个例子极深刻地显示了该隐蔽性:

    < html >
        
    < head >
            
    < script language = " JScript " >

            
    function  AttachEvents(element)
            {
                
    //  This structure causes element to ref ClickEventHandler  //element有个引用指向函数ClickEventHandler()
                element.attachEvent( " onclick " , ClickEventHandler);

                
    function  ClickEventHandler()
                {
                    
    //  This closure refs element  //该函数有个引用指向AttachEvents(element)调用Scope,也就是执行了参数element。
                    
                }
            }

            
    function  SetupLeak()
            {
                
    //  The leak happens all at once
                AttachEvents(document.getElementById( " LeakedDiv " ));
            }

            
    </ script >
        
    </ head >

        
    < body onload = " SetupLeak() "  onunload = " BreakLeak() " >
            
    < div id = " LeakedDiv " ></ div >
        
    </ body >
    </ html >

    还有这个例子在IE 6中同样原因会引起泄露



    function  leakmaybe() {
    var  elm  =  document.createElement( " DIV " );
      elm.onclick 
    =   function () {
    return   2   +   2 ;
      }
    }

    for  ( var  i  =   0 ; i   10000 ; i ++ ) {
      leakmaybe();
    }


    btw:
    关于Closure的知识,大家可以看看http://jibbering.com/faq/faq_notes/closures.html这篇文章,习惯中文也可以看看zkjbeyond的blog,他对Closure这篇文章进行了简要的翻译:http://www.blogjava.net/zkjbeyond/archive/2006/05/19/47025.html。之所以会有这一系列的问题,关键就在于javascript是种函数式脚本解析语言,因此javascript中“函数中的变量的作用域是定义作用域,而不是动态作用域”,这点在犀牛书《JavaScript: The Definitive Guide》中的“Funtion”一章中有所讨论。
    http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555中也对这个问题举了很详细的例子。


    一些 简单的解决方案

    目前大多数ajax前端的javascript framework都利用对事件的管理,解决了该问题。

    如果你需要自己解决这个问题,可以参考以下的一些方法:

    • http://youngpup.net/2005/0221010713 中提到:可以利用递归Dom树,解除event绑定,从而解除循环引用:

      														
      if (window.attachEvent) {
      var clearElementProps = [
      'data',
      'onmouseover',
      'onmouseout',
      'onmousedown',
      'onmouseup',
      'ondblclick',
      'onclick',
      'onselectstart',
      'oncontextmenu'
      ];

      window.attachEvent("onunload", function() {
      var el;
      for(var d = document.all.length;d--;){
      el = document.all[d];
      for(var c = clearElementProps.length;c--;){
      el[clearElementProps[c]] = null;
      }
      }
      });
      }
    • http://novemberborn.net/javascript/event-cache一文中则通过增加EventCache,从而给出一个相对结构化的解决方案

      /*     EventCache Version 1.0
          Copyright 2005 Mark Wubben

          Provides a way for automagically removing events from nodes and thus preventing memory leakage.
          See <http://novemberborn.net/javascript/event-cache> for more information.
          
          This software is licensed under the CC-GNU LGPL <http://creativecommons.org/licenses/LGPL/2.1/>
      */

      /*     Implement array.push for browsers which don't support it natively.
          Please remove this if it's already in other code 
      */
      if (Array.prototype.push  ==   null ){
          Array.prototype.push 
      =   function (){
              
      for ( var  i  =   0 ; i  <  arguments.length; i ++ ){
                  
      this [ this .length]  =  arguments[i];
              };
              
      return   this .length;
          };
      };

      /*     Event Cache uses an anonymous function to create a hidden scope chain.
          This is to prevent scoping issues. 
      */
      var  EventCache  =   function (){
          
      var  listEvents  =  [];
          
          
      return  {
              listEvents : listEvents,
          
              add : 
      function (node, sEventName, fHandler, bCapture){
                  listEvents.push(arguments);
              },
          
              flush : 
      function (){
                  
      var  i, item;
                  
      for (i  =  listEvents.length  -   1 ; i  >=   0 ; i  =  i  -   1 ){
                      item 
      =  listEvents[i];
                      
                      
      if (item[ 0 ].removeEventListener){
                          item[
      0 ].removeEventListener(item[ 1 ], item[ 2 ], item[ 3 ]);
                      };
                      
                      
      /*  From this point on we need the event names to be prefixed with 'on"  */
                      
      if (item[ 1 ].substring( 0 2 !=   " on " ){
                          item[
      1 =   " on "   +  item[ 1 ];
                      };
                      
                      
      if (item[ 0 ].detachEvent){
                          item[
      0 ].detachEvent(item[ 1 ], item[ 2 ]);
                      };
                      
                      item[
      0 ][item[ 1 ]]  =   null ;
                  };
              }
          };
      }();

    • 使用方法也很简单:

      												
      <script type="text/javascript">
      function addEvent(oEventTarget, sEventType, fDest){
      if(oEventTarget.attachEvent){
      oEventTarget.attachEvent("on" + sEventType, fDest);
      } elseif(oEventTarget.addEventListener){
      oEventTarget.addEventListener(sEventType, fDest, true);
      } elseif(typeof oEventTarget[sEventType] == "function"){
      var fOld = oEventTarget[sEventType];
      oEventTarget[sEventType] = function(e){ fOld(e); fDest(e); };
      } else {
      oEventTarget[sEventType] = fDest;
      };

      /* Implementing EventCache for all event systems */
      EventCache.add(oEventTarget, sEventType, fDest, true);
      };


      function createLeak(){
      var body = document.body;

      function someHandler(){
                     return body;
      };

      addEvent(body, "click", someHandler);
      };

      window.onload = function(){
      var i = 500;
      while(i > 0){
      createLeak();
      i = i - 1;
      }
      };

      window.onunload = EventCache.flush;
      </script>
    • http://talideon.com/weblog/2005/03/js-memory-leaks.cfm 一文中的方法类似:

      /*
       * EventManager.js
       * by Keith Gaughan
       *
       * This allows event handlers to be registered unobtrusively, and cleans
       * them up on unload to prevent memory leaks.
       *
       * Copyright (c) Keith Gaughan, 2005.
       *
       * All rights reserved. This program and the accompanying materials
       * are made available under the terms of the Common Public License v1.0
       * (CPL) which accompanies this distribution, and is available at
       * http://www.opensource.org/licenses/cpl.php
       *
       * This software is covered by a modified version of the Common Public License
       * (CPL), where Keith Gaughan is the Agreement Steward, and the licensing
       * agreement is covered by the laws of the Republic of Ireland.
       
      */

      //  For implementations that don't include the push() methods for arrays.
      if  ( ! Array.prototype.push) {
          Array.prototype.push 
      =   function (elem) {
              
      this [ this .length]  =  elem;
          }
      }

      var  EventManager  =  {
          _registry: 
      null ,

          Initialise: 
      function () {
              
      if  ( this ._registry  ==   null ) {
                  
      this ._registry  =  [];

                  
      //  Register the cleanup handler on page unload.
                  EventManager.Add(window,  " unload " this .CleanUp);
              }
          },

          
      /* *
           * Registers an event and handler with the manager.
           *
           * @param  obj         Object handler will be attached to.
           * @param  type        Name of event handler responds to.
           * @param  fn          Handler function.
           * @param  useCapture  Use event capture. False by default.
           *                     If you don't understand this, ignore it.
           *
           * @return True if handler registered, else false.
           
      */
          Add: 
      function (obj, type, fn, useCapture) {
              
      this .Initialise();

              
      //  If a string was passed in, it's an id.
               if  ( typeof  obj  ==   " string " ) {
                  obj 
      =  document.getElementById(obj);
              }
              
      if  (obj  ==   null   ||  fn  ==   null ) {
                  
      return   false ;
              }

              
      //  Mozilla/W3C listeners?
               if  (obj.addEventListener) {
                  obj.addEventListener(type, fn, useCapture);
                  
      this ._registry.push({obj: obj, type: type, fn: fn, useCapture: useCapture});
                  
      return   true ;
              }

              
      //  IE-style listeners?
               if  (obj.attachEvent  &&  obj.attachEvent( " on "   +  type, fn)) {
                  
      this ._registry.push({obj: obj, type: type, fn: fn, useCapture:  false });
                  
      return   true ;
              }

              
      return   false ;
          },

          
      /* *
           * Cleans up all the registered event handlers.
           
      */
          CleanUp: 
      function () {
              
      for  ( var  i  =   0 ; i  <  EventManager._registry.length; i ++ ) {
                  
      with  (EventManager._registry[i]) {
                      
      //  Mozilla/W3C listeners?
                       if  (obj.removeEventListener) {
                          obj.removeEventListener(type, fn, useCapture);
                      }
                      
      //  IE-style listeners?
                       else   if  (obj.detachEvent) {
                          obj.detachEvent(
      " on "   +  type, fn);
                      }
                  }
              }

              
      //  Kill off the registry itself to get rid of the last remaining
               //  references.
              EventManager._registry  =   null ;
          }
      };

      使用起来也很简单

      												
      <html>
      <head>
      <script type=text/javascript src=EventManager.js></script>
      <script type=text/javascript>
      function onLoad() {

      EventManager.Add(document.getElementById(testCase),click,hit );
      returntrue;
      }

      function hit(evt) {
      alert(click);
      }
      </script>
      </head>

      <body onload='javascript: onLoad();'>

      <div id='testCase' style='100%; height: 100%; background-color: yellow;'>
      <h1>Click me!</h1>
      </div>

      </body>
      </html>
    • google map api同样提供了一个类似的函数用在页面的unload事件中,解决Closure带来的内存泄露问题。
    • 当然,如果你不嫌麻烦,你也可以为每个和native object有关的就阿vascript object编写一个destoryMemory函数,用来手动调用,从而手动解除Dom对象的事件绑定。

    Cross-Page Leaks

        Cross-Page Leaks和下一节提到的Pseudo-Leaks在我看来,就是IE的bug, 虽然MS死皮赖脸不承认:)

         大家可以看看这段例子代码:

    < html >
        
    < head >
            
    < script language = " JScript " >

            
    function  LeakMemory()  // 这个函数会引发Cross-Page Leaks
            {
                
    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()  // 而这个函数不会引发Cross-Page Leaks
            {
                
    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 onclick = " LeakMemory() " > Memory Leaking Insert </ button >
            
    < button onclick = " CleanMemory() " > Clean Insert </ button >
            
    < div id = " hostElement " ></ div >
        
    </ body >
    </ html >

    LeakMemory和CleanMemory这两段函数的唯一区别就在于他们的代码的循序,从代码上看,两段代码的逻辑都没有错。

    但LeakMemory却会造成泄露。原因是LeakMemory()会先建立起parentDiv和childDiv之间的连接,这时候,为了让 childDiv能够获知parentDiv的信息,因此IE需要先建立一个临时的scope对象。而后parentDiv建立了和 hostElement对象的联系,parentDiv和childDiv直接使用页面document的scope。可惜的是,IE不会释放刚才那个临时的scope对象的内存空间,直到我们跳转页面,这块空间才能被释放。而CleanMemory函数不同,他先把parentDiv和 hostElement建立联系,而后再把childDiv和parentDiv建立联系,这个过程不需要单独建立临时的scope,只要直接使用页面 document的scope就可以了, 所以也就不会造成内存泄露了

    详细原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp这篇文章。

    btw:
    IE 6中垃圾回收算法,就是从那些直接"in scope"的对象开始进行mark清除的:
    Every variable which is "in scope" is called a "scavenger". A scavenger may refer to a number, an object, a string, whatever. We maintain a list of scavengers – variables are moved on to the scav list when they come into scope and off the scav list when they go out of scope.

    Pseudo-Leaks

    这个被称为“秀逗泄露”真是恰当啊:)
    看看这个例子:

    < html >
        
    < head >
            
    < script language = " JScript " >

            
    function  LeakMemory()
            {
                
    //  Do it a lot, look at Task Manager for memory response

                
    for (i  =   0 ; i  <   5000 ; i ++ )
                {
                    hostElement.text 
    =   " function foo() { } " ;//看内存会不断增加
                }
            }
            
    </ script >
        
    </ head >

        
    < body >
            
    < button onclick = " LeakMemory() " > Memory Leaking Insert </ button >
            
    < script id = " hostElement " > function  foo() { } </ script >
        
    </ body >
    </ html >

    MS是这么解释的,这不是内存泄漏。如果您创建了许多无法获得也无法释放的对象,那才是内存泄漏。在这里,您将创建许多元素,Internet Explorer 需要保存它们以正确呈现页面。Internet Explorer 并不知道您以后不会运行操纵您刚刚创建的所有这些对象的脚本。当页面消失时(当您浏览完,离开浏览器时)会释放内存。它不会泄漏。当销毁页面时,会中断循环引用。

    唉~~~

    详细原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp这篇文章。

    其它一些琐碎的注意点

    变量定义一定要用var,否则隐式声明出来的变量都是全局变量,不是局部变量;
    全局变量没用时记得要置null;
    注意正确使用delete,删除没用的一些函数属性;
    注意正确使用try...cache,确保去处无效引用的代码能被正确执行;
    open出来的窗口即使close了,它的window对象还是存在的,要记得删除引用;
    frame和iframe的情况和窗口的情况类似。

  • 相关阅读:
    Java知识15 Number&Math类【多测师】
    python callable()方法实例
    高级super实例
    高级any、for组合用法
    python 字典update、setdefault、pop方法案例
    一个经典的python字典生成式案例
    一个发挥到极致的yield案例
    python map使用
    Python yield详解
    django __path__使用
  • 原文地址:https://www.cnblogs.com/pricks/p/1672479.html
Copyright © 2011-2022 走看看