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

GC与JS内存泄露

阅读更多

原文地址:http://www.aliued.cn/?p=2908#more-2908

 

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种,一是清楚后对内存 进行复制整理,就像磁盘整理程序那样,把所有还在使用的内存移到一起,把释放掉的内存移到一起,如图:1271568092

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

第二种方案是不移动内存,而是按大小分类,建立一系链表,把这些碎片按大小连接并管理起来,(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,循环引用出现,内存泄露,其原理如下:

11111

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

  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对象,产生了一个作用域的循环引用,从而造成内存泄露。其原理图如下:11111

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

  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,大家记住就可以了,不需要过分深究。其原理如下:

22222
四、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造成的内存泄露不是程序和项目的瓶颈,我们需要在各方面进行权衡。

分享到:
评论

相关推荐

    Node.js中内存泄漏分析

    内存泄漏(MemoryLeak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。如果内存泄漏的位置比较关键,那么随着处理的进行可能持有越来越多的无用内存,这些无用的内存...Java、JavaScript由于使用了GC(G

    备忘录java源码-javascript-memory-leakage:检测导致垃圾收集JavaScript应用程序中的内存泄漏的源代码的简单

    备注java原始码寻找JavaScript应用程序中的内存泄漏 垃圾回收是程序通过回收未在程序中使用的对象占用的内存来执行自动内存管理的过程。 内存泄漏是指程序不需要的内存未返回操作系统或空闲内存池时的情况。 有关更...

    iojs_tls_bug:显示某些 HTTPS 实现的内存泄漏

    它还在测量之前使用 global.gc() 来测量实际泄漏。 指示 如有必要,修复流浪路径问题 sudo chown -R vagrant ~ sudo chgrp -R vagrant ~ 安装 nvm install 0.10 # or "0.12", or "iojs" rm -rf ./node_modules npm...

    fs.readfile-memory-leak.jest

    执行测试用例 yarn test-gc错误如果将LEAKY依赖项之一注释到Leak.js中,则不再释放每个文件的内存。泄漏// const fs = require('fs-extra'); //TODO: LEAKY// const fs = require('graceful-fs'); //TODO: LEAKY// ...

    riotleaksample:Riotjs中的内存泄漏示例

    riotleaksample Riotjs中的内存泄漏示例设置npm install .建造npm run build符文npm run start#Error我收到以下错误。 &lt;--- Last few GCs ---&gt; 26707 ms: Scavenge 1397.9 (1456.9) -&gt; 1397.9 (1456.9) MB, ...

    LLJS:LLJS:低级JavaScript

    它可以编译为JavaScript,使您可以轻松编写高效内存和无GC暂停的代码,简而言之,LLJS是JavaScript和C的混蛋。LLJS是早期研究的原型工作,所以不要指望任何坚固的工具。 这里的研究目标是探索高级动态类型语言中的...

    火炮:ARTILLERY-一种现代的载荷测试和功能测试工具包。 受到全球开发人员,质量检查工程师和SRE的喜爱

    分析和调试性能问题-运行负载测试以分析和修复高CPU使用率,大量GC暂停,内存泄漏以及配置错误的资源池 语义监视-针对生产API连续运行现实的用户方案,并在错误的响应或错误时发出警报 产品特点 多种协议:负载测试...

    heapdump-cleanup

    堆转储清理对Node堆快照进行后处理,以删除所有WeakMap强边缘,从而更容易跟踪从可疑泄漏对象到GC根的路径。用法 npx heapdump-cleanup &lt;input&gt; 局限性该脚本读取内存中的整个堆转储以进行处理。 读取和写入是...

    千方百计笔试题大全

    31、java 中会存在内存泄漏吗,请简单描述。 11 32、abstract 的method 是否可同时是static,是否可同时是native,是否可同时是synchronized? 11 33、静态变量和实例变量的区别? 11 34、是否可以从一个static 方法...

    java面试宝典

    31、java 中会存在内存泄漏吗,请简单描述。 11 32、abstract 的method 是否可同时是static,是否可同时是native,是否可同时是synchronized? 11 33、静态变量和实例变量的区别? 11 34、是否可以从一个static 方法...

    Java面试宝典2020修订版V1.0.1.doc

    43、什么是java内存泄漏,怎么预防? 85 七、框架部分 85 1、谈谈你对Struts2的理解。 85 2、谈谈你对Hibernate的理解。 86 3、你对Spring的理解。 87 4、Struts2优缺点 87 5、ORM工作原理? 89 6、struts2的核心...

    最新Java面试宝典pdf版

    81、java中会存在内存泄漏吗,请简单描述。 53 82、能不能自己写个类,也叫java.lang.String? 57 83. Java代码查错 57 二. 算法与编程 61 1、编写一个程序,将a.txt文件中的单词与b.txt文件中的单词交替合并到c.txt...

    Java面试笔试资料大全

    81、java中会存在内存泄漏吗,请简单描述。 53 82、能不能自己写个类,也叫java.lang.String? 57 83. Java代码查错 57 二. 算法与编程 61 1、编写一个程序,将a.txt文件中的单词与b.txt文件中的单词交替合并到c.txt...

    Java面试宝典2010版

    81、java中会存在内存泄漏吗,请简单描述。 82、能不能自己写个类,也叫java.lang.String? 83. Java代码查错 二. 算法与编程 1、编写一个程序,将a.txt文件中的单词与b.txt文件中的单词交替合并到c.txt文件中,...

    Java面试宝典-经典

    81、java中会存在内存泄漏吗,请简单描述。 53 82、能不能自己写个类,也叫java.lang.String? 57 83. Java代码查错 57 二. 算法与编程 61 1、编写一个程序,将a.txt文件中的单词与b.txt文件中的单词交替合并到c.txt...

    JAVA面试宝典2010

    81、java中会存在内存泄漏吗,请简单描述。 53 82、能不能自己写个类,也叫java.lang.String? 57 83. Java代码查错 57 二. 算法与编程 61 1、编写一个程序,将a.txt文件中的单词与b.txt文件中的单词交替合并到c.txt...

    java面试题大全(2012版)

    81、java中会存在内存泄漏吗,请简单描述。 53 82、能不能自己写个类,也叫java.lang.String? 57 83. Java代码查错 57 二. 算法与编程 61 1、编写一个程序,将a.txt文件中的单词与b.txt文件中的单词交替合并到c.txt...

    Java面试宝典2012版

    81、java中会存在内存泄漏吗,请简单描述。 53 82、能不能自己写个类,也叫java.lang.String? 57 83. Java代码查错 57 二. 算法与编程 61 1、编写一个程序,将a.txt文件中的单词与b.txt文件中的单词交替合并到c...

    java面试宝典2012

    81、java中会存在内存泄漏吗,请简单描述。 59 82、能不能自己写个类,也叫java.lang.String? 62 83. Java代码查错 63 二. 算法与编程 67 1、编写一个程序,将a.txt文件中的单词与b.txt文件中的单词交替合并到c.txt...

Global site tag (gtag.js) - Google Analytics