文章导航PC6首页软件下载单机游戏安卓资源苹果资源

pc软件新闻网络操作系统办公工具编程服务器软件评测

安卓新闻资讯应用教程刷机教程安卓游戏攻略tv资讯深度阅读综合安卓评测

苹果ios资讯苹果手机越狱备份教程美化教程ios软件教程mac教程

单机游戏角色扮演即时战略动作射击棋牌游戏体育竞技模拟经营其它游戏游戏工具

网游cf活动dnf活动lol周免英雄lol礼包

手游最新动态手游评测手游活动新游预告手游问答

您的位置:首页资讯编程开发 → GC与JS内存泄漏

GC与JS内存泄漏

时间:2010/9/23 22:00:04来源:本站整理作者:前段观察我要评论(0)

Javascript有没有内存泄露?如果有,如何避免?鉴于最近有好几个人问到我类似的问题,看来大家对这部分内容还没有系统的研究过,因此,打算在这里把个人几年前整理的一些资料和大家分享一下.

首先,可以肯定的说,javascript的一些写法会造成内存泄露的,至少在IE6下如此.因此,在IE6迟迟不肯退休的今天,我们还是有必要了解相关的知识(虽然大部分情况下,js造成的这点内存泄露不是致使电脑运行变慢的主要原因).相关的研究主要集中在05-07这几年,本文并没有什么新的观点,如果当年有研究过的朋友,可以直接忽略.

作为前端开发人员,了解这些问题的时候,需要知其然也知其所以然,因此,在介绍js内存泄露前,我们先从为什么会有内存泄露谈起.

说道内存泄露,就不得不谈到内存分配的方式.内存分配有三种方式,分别是:

一、静态分配( Static Allocation ):静态变量和全局变量的分配形式.如果把房间看做一个程序,我们可以把静态分配的内存当成是房间里的耐用家具.通常,它们无需释放和回收,因为没人会天天把大衣柜当作垃圾扔到窗外.

二、自动分配( Automatic Allocation ):在栈中为局部变量分配内存的方法.栈中的内存可以随着代码块退出时的出栈操作被自动释放.这类似于到房间中办事的人,事情一旦完成,就会自己离开,而他们所占用的空间,也随着这些人的离开而自动释放了.

三、动态分配( Dynamic Allocation ):在堆中动态分配内存空间以存储数据的方式.也就是程序运行时用malloc或new申请的内存,我们需要自己用free或delete释放.动态内存的生存期由程序员自己决定.一旦忘记释放,势必造成内存泄露.这种情况下,堆中的内存块好像我们日常使用的餐巾纸,用过了就得扔到垃圾箱里,否则屋内就会满地狼藉.因此,懒人们做梦都想有一台家用机器人跟在身边打扫卫生.在软件开发中,如果你懒得释放内存,那么你也需要一台类似的机器人——这其实就是一个由特定算法实现的垃圾收集器.而正是垃圾收集机制本身的一些缺陷,导致了javascript内存泄露.

几年前看过一篇叫《垃圾回收趣史》的文章,里面对垃圾回收机制进行了深入浅出的说明.

就像机械增压这种很多豪车作为卖点的技术,其实上个世纪10年代奔驰就在使用了一样,垃圾回收技术诞生也有很长的时间了.1960 年前后诞生于 MIT 的 Lisp 语言是第一种高度依赖于动态内存分配技术的语言,Lisp 中几乎所有数据都以“表”的形式出现,而“表”所占用的空间则是在堆中动态分配得到的. Lisp 语言先天就具有的动态内存管理特性要求 Lisp 语言的设计者必须解决堆中每一个内存块的自动释放问题(否则,Lisp 程序员就必然被程序中不计其数的 free 或 delete 语句淹没),这直接导致了垃圾收集技术的诞生和发展.

而三种最基本的垃圾回收算法,也在那个时候一起出现了.下面我们一个一个了解一下:

引用计数(Reference Counting)算法:这个可能是最早想到的方法.形象点说,引用计数可以这么理解,房子里放了很多白纸,这些纸就好比是内存.使用内存,就好比在这些纸上写字.内存可以随便使用,但是,有个条件,任何使用一张纸的人,必须在纸的一角写上计数1,如果2个人同时使用一张纸,那么计数就变成2,以此类推.当一个人使用完某张纸的时候,必须把角上的计数减1,这样,一旦当计数变为0,就满足了垃圾回收条件,等在一旁的机器人会立即把这张纸扔进垃圾箱.基于引用计数器的垃圾收集器运行较快,不会长时间中断程序执行,适宜地必须 实时运行的程序.但引用计数器增加了程序执行的开销;同时,还有个最大的问题,这个算法存在一个缺陷,就是一旦产生循环引用,内存就会被泄露.举个例子,我们new了2个对象a和b,这时,a和b的计数都是1,然后,我们把a的一个属性指向b,b的一个属性指向a,此时,由于引用的关系,a和b的计数都变成了2,当程序运行结束时,退出作用域,程序自动把a的计数减1,由于最后a的计数仍然为1,因此,a不会被释放,同样,b最后的计数也为1,b也不会被释放,内存就这么泄露了!

标记-清除(Mark-Sweep)算法:同样是房间和白纸的例子,这次规则有所修改.白纸仍然随便用,并且,一开始,不需要做什么记号,但是用到某个时候,机器人会突然命令所有人停下来,这时,需要每个人在自己仍然需要使用的白纸上做一个记号,大家都做完记号后,机器人会把那些没有记号的白纸全部扔进垃圾箱.正如其名称所暗示的那样,标记-清除算法的执行过程分为“标记”和“清除”两大阶段.这种分步执行的思路奠定了现代垃圾收集算法的思想基础.与引用计数算法不同的是,标记-清除算法不需要运行环境监测每一次内存分配和指针操作,而只要在“标记”阶段中跟踪每一个指针变量的指向——用类似思路实现的垃圾收集器也常被后人统称为跟踪收集器( Tracing Collector ).当然,标记-清楚算法的缺陷也很明显,首先是效率问题,为了标记,必须暂停程序,长时间进行等待,其次,标记清除算法会造成内存碎片,比如被标记清除的只是一些很小的内存块,而我们接下来要申请的都是一些大块的内存,那么刚才清除掉的内存,其实还是无法使用.解决方案,常见的有2种,一是清楚后对内存进行复制整理,就像磁盘整理程序那样,把所有还在使用的内存移到一起,把释放掉的内存移到一起,如图:

但是,这样一来效率就更低了.

第二种方案是不移动内存,而是按大小分类,建立一系链表,把这些碎片按大小连接并管理起来,(4个字节的内存一个链表,8个字节的内存一个链表……)如果我们需要4个字节的内存,就从4个字节的链表里面去取,需要16个字节,就从16字节的链表里面去取,只有到了一定时候,比如程序空闲或者大块的内存空间不足,才会去整理合并这些碎片.

为什么重点谈mark-sweep算法呢,主要是ie对javascript的垃圾回收,采用的就是这种算法.

复制(copying)算法:mark-sweep算法效率低下,由此,又产生了一种新的奇思妙想,我们再把规则换一下:还是房间和白纸的例子,这次我们把房间分成左右2部分,一开始,所有人都在左边,白纸仍然随便用,一定时候,机器人又会叫大家停下来,这次不做记号了,你只要带着你还需要的白纸转移到右边去就可以了(相当于把现有的程序复制一份,无法使用的部分自然不会被复制),那些没用的纸自然就剩了下来,然后机器人会把左边所有的垃圾打扫干净(相当于把原先使用的那一半内存直接清空),下次执行垃圾回收的时候采用同样的方式,只不过这次从右边向左边迁移.这种算法的效率奇高,可惜,对内存的消耗太大,尤其是在1960年,内存可比黄金贵多了,直接砍掉一半的内存,显然是无法接受的.

了解万垃圾回收算法,再来看看IE下为什么会产生内存泄露.

在IE 6中,对于javascript object内部,javascript使用的是mark-and-sweep算法,这点前面也有提到,因此,纯粹的javascript对象的使用,不会造成内存泄露,但是对于javascript object与外部object(包括native object和vbscript object等等)的引用时,IE 6使用引用计数,这样一来,内存泄露就产生了.这点在犀牛书第八章函数部分有提到.

以下是常见的几种javascript内存泄露的情况:

一、循环引用:

  1.   <html>
  2.      <head>
  3.          < script language ="JScript">
  4.          var  myGlobalObject;
  5.          function  SetupLeak()  // 产生循环引用,因此会造成内存泄露
  6.         {
  7.              //  First set up the script scope to element reference
  8.             myGlobalObject  = document.getElementById("LeakedDiv");
  9.              //  Next set up the element to script scope reference
  10.             document.getElementById("LeakedDiv").expandoProperty  =  myGlobalObject;
  11.         }
  12.          
  13.      </head>
  14.      <body onload = "SetupLeak()">
  15.          <div id ="LeakedDiv" ></div>
  16.      </body>
  17.  </html>

我们可以看到,myGlobalObject指向了一个DOM对象,而这个DOM对象的一个属性又指向了myGlobalObject,循环引用出现,内存泄露,其原理如下:

解决方案很简单,在确保属性不再使用后,加入以下代码就可以了:

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

说起来容易,不过当我们程序非常复杂的时候,发现和修改就没有这么容易了.

二、闭包(Closures)

仍然先看一段代码:

  1. <html>
  2.      <head>
  3.          <script language="JScript">
  4.          function  AttachEvents(element)
  5.         {
  6.              //  This structure causes element to ref ClickEventHandler  
  7.             element.attachEvent( " onclick " , ClickEventHandler); function  ClickEventHandler()
  8.             {
  9.                  //  This closure refs element  
  10.                
  11.             }
  12.         } function  SetupLeak()
  13.         {
  14.              //  The leak happens all at once
  15.             AttachEvents(document.getElementById( " LeakedDiv " ));
  16.         }
  17.         </script>
  18.      </head> <body onload="SetupLeak()">
  19.          <div id="LeakedDiv"></div>
  20.      </body>
  21. </html>

闭包的一个内部方法赋给了element对象,产生了一个作用域的循环引用,从而造成内存泄露.其原理图如下:

 

解决方案如下,在确定事件不再使用后,解除事件的绑定:

  1. function BreakLeak() {
  2.      document.getElementById(”LeakedDiv”).detachEvent(”onclick”, document.getElementById(”LeakedDiv”).expandoClick);  
  3.      document.getElementById(”LeakedDiv”).expandoClick = null;
  4. }

通常情况下,常用的js框架都帮我们解决了这个问题,不需要我们自己处理,这也是使用框架的一个好处.

三、Cross-Page-Leaks

仍然先看一个例子:

  1. <html>
  2.      <head>
  3.          <script language="JScript">
  4.          function  LeakMemory()  
  5.         {
  6.              var  hostElement  =  document.getElementById("hostElement"); //  Do it a lot, look at Task Manager for memory response
  7.  
  8.              for (i  =   0 ; i  < 5000 ; i ++ )
  9.             {
  10.                  var  parentDiv  =
  11.                     document.createElement("<div onClick='foo()'>");
  12.                  var  childDiv  =
  13.                     document.createElement("<div onClick='foo()'>"); //  This will leak a temporary object
  14.                 parentDiv.appendChild(childDiv);
  15.                 hostElement.appendChild(parentDiv);
  16.                 hostElement.removeChild(parentDiv);
  17.                 parentDiv.removeChild(childDiv);
  18.                 parentDiv  =   null ;
  19.                 childDiv  =   null ;
  20.             }
  21.             hostElement  =   null ;
  22.         } function  CleanMemory()  
  23.         {
  24.              var  hostElement  =  document.getElementById("hostElement"); //  Do it a lot, look at Task Manager for memory response
  25.  
  26.              for (i  =   0 ; i  < 5000 ; i ++ )
  27.             {
  28.                  var  parentDiv  =   document.createElement("<div onClick='foo()'>");
  29.                  var  childDiv  =   document.createElement("<div onClick='foo()'>"); //  Changing the order is important, this won’t leak
  30.                 hostElement.appendChild(parentDiv);
  31.                 parentDiv.appendChild(childDiv);
  32.                 hostElement.removeChild(parentDiv);
  33.                 parentDiv.removeChild(childDiv);
  34.                 parentDiv  =   null ;
  35.                 childDiv  =   null ;
  36.             }
  37.             hostElement  =   null ;
  38.         }
  39.          </div></div></script>
  40.      </head>
  41.      <body>
  42.          <button onclick ="LeakMemory()"> Memory Leaking Insert </button>
  43.          <button onclick ="CleanMemory()"> Clean Insert </button>
  44.          <div id ="hostElement"></div>
  45.      </body>
  46. </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就可以了, 所以也就不会造成内存泄露了.但是,需要特别说明一下,如果LeakMemory方法里面,创建的div对象上不绑定script事件,那么也不会有泄漏,这个可以理解为ie的bug,大家记住就可以了,不需要过分深究.其原理如下:

四、Pseudo-Leaks:

同样可以理解为ie的bug的一种泄露:

  1. <html>
  2.     <head>
  3.         <script language="JScript">
  4.  
  5.         function LeakMemory()
  6.         {
  7.             // Do it a lot, look at Task Manager for memory response
  8.  
  9.             for(i = 0; i < 5000; i++)
  10.             {
  11.                 hostElement.text = “function foo() { }”;
  12.             }
  13.         }
  14.         </script>
  15.     </script></head>
  16.  
  17.     <body>
  18.         <button onclick=”LeakMemory()”>Memory Leaking Insert</button>
  19.         <script id=”hostElement”>function foo() { }</script>
  20.     </body>
  21. </html>

没什么特别的好解释,记住就可以了.

关于这四种泄漏的具体描述,还是请各位参照原文:http://msdn.microsoft.com/en-us/library/Bb250448

以上是几种主要的泄露,当然,除此之外,网上还有一些其他的讨论,比如var str = "lalala";alert(str.length);这个简单的语句也会造成内存泄露,原因是类型转换的时候,ie生成了一个临时对象,这个临时对象被泄漏了.类似情况还有很多,大家有兴趣可以自己去搜集整理.

最后说一下,只要ie6还健在,作为前端开发人员,就不能逃避这些问题,当然,也不必过分深究,比如闭包的情况就比较难避免,就像我一开始说的,毕竟,javascript造成的内存泄露不是程序和项目的瓶颈,我们需要在各方面进行权衡.

 

相关视频

    没有数据

相关阅读 QQ怎么节省内存空间 手机QQ内存怎么清理看门狗2内存不足怎么办 看门狗2cpu使用率调低教程Chrome浏览器占用内存过大怎么办?一招解决谷歌浏览器内存占用腾讯嘉年华TGC2016憋大招? 代号零完全公开iPhone7运行内存多大 iPhone7多少运行内存iPhone 7售价 32GB内存只卖16GB的价格星界边境内存条在哪里 星界边境内存条获得方法苹果7配3G内存跑分性能怎么样 苹果7配3G内存跑分性能无敌

文章评论
发表评论

热门文章 没有查询到任何记录。

最新文章 编程语言排行榜2020年android studio怎么删 eclipse怎么设置编码格式 eclipse设置编码格andriod studio如何使用真机测试 andriod sandroid studio怎么生成apk android studioandroid studio如何导入jar包 android stud

人气排行 安卓模拟器BlueStacks安装使用教程编程语言排行榜2020年9月 TIOBE编程语言排行eclipse字体大小怎么设置 eclipse字体大小plsql developer怎么连接数据库 plsql deveTomcat9.0安装教程 Tomcat9.0环境变量配置方plsql developer怎么使用 plsql developerVisual Studio 2015环境搭建教程Eclipse优化设置教程 Eclipse优化设置技巧