`
kongxiantao
  • 浏览: 112870 次
  • 性别: 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造成的内存泄露不是程序和项目的瓶颈,我们需要在各方面进行权衡。

分享到:
评论

相关推荐

    js内存泄露问题

    JavaScript内存泄露是一个重要的性能优化话题,尤其是在开发大型的、长时间运行的Web应用时。内存泄露会导致应用程序占用过多的系统资源,影响用户体验,甚至可能导致浏览器崩溃。本文将深入探讨JavaScript内存泄露...

    javascript 内存泄漏 检测 解决 检测工具 原因分析

    JavaScript内存泄漏是一个重要的编程问题,尤其对于Web应用来说,它可能导致性能下降,用户界面响应变慢,甚至在极端情况下导致应用程序崩溃。理解内存泄漏的原因、如何检测和解决它们是每个JavaScript开发者必备的...

    Js内存泄漏及解决方案

    在JavaScript中,内存泄漏是指由于某种原因导致不再使用的变量或对象仍然占据着内存空间,从而无法被垃圾回收机制(Garbage Collection, GC)释放,这将导致应用程序逐渐消耗更多的内存,最终可能导致性能下降甚至...

    内存泄漏检测工具

    内存泄漏是程序运行过程中,不再使用的内存没有被正确释放,导致系统资源持续占用,从而影响程序性能甚至系统...了解并掌握这些知识点,开发者可以更有效地预防和解决JavaScript内存泄漏问题,确保应用的高效稳定运行。

    内存泄露,闭包 内存泄露,闭包 内存泄露,闭包

    在JavaScript中,由于其自动垃圾回收(Garbage Collection, GC)机制,程序员通常不会直接管理内存,但不恰当的编程习惯可能导致内存泄露。例如,全局变量、循环引用和闭包都可能导致内存无法被正确回收。 闭包是...

    01-JS内存泄漏.md

    JS内存泄漏是前端开发过程中经常遇到的问题,它指的是程序中已分配的内存由于某些原因未被释放或者无法释放,导致应用程序可用的内存逐渐减少。本节将详细探讨JS内存泄漏的原因、检测方法以及如何预防和解决内存泄漏...

    藏经阁-穆客带你快速定位 Node.js 内存泄露.pdf

    【Node.js内存泄露详解】 Node.js是一个基于Chrome V8引擎的JavaScript运行环境,因其事件驱动、非阻塞I/O的特性而备受青睐,尤其适用于构建Web应用和API服务器。其高效的性能使得许多企业和开发者选择使用Node.js...

    JavaScript避开内存泄露及内存管理技巧_.docx

    JavaScript 避免内存泄露及内存管理技巧 在JavaScript开发中,内存泄露是一个常见的问题,它会导致页面崩溃、性能下降和用户体验不良。因此,了解如何避免内存泄露和有效地管理内存是非常重要的。本文将详细讲解...

    【JavaScript源代码】一篇文章弄懂javascript内存泄漏.docx

    【JavaScript源代码】一篇文章弄懂javascript内存泄漏 在JavaScript中,内存管理对于程序性能至关重要,因为内存泄漏会导致程序效率下降,甚至可能导致应用崩溃。本文旨在深入解析JavaScript中的内存泄漏及其解决...

    唯快不破——高效定位线上Node.js应用内存泄漏.pdf

    V8是Node.js的底层JavaScript引擎,因此理解V8的GC过程对于优化Node.js应用性能及排查内存泄漏至关重要。V8中的堆内存被划分为几个区域,包括: - CodeSpace:存放编译后的代码。 - MapSpace:存放对象指向的隐藏类...

    js内存管理及优化总结和文档

    由于JavaScript是动态类型语言,其内存管理与编译型语言有所不同,因此理解JS的内存管理和优化对于提升应用程序性能至关重要。 **一、JS内存管理** JS的内存管理主要依赖于垃圾收集机制(Garbage Collection, GC)...

    藏经阁-穆客带你快速定位Node.js内存泄露.pdf

    【Node.js内存管理与内存泄露】 Node.js是一款基于Chrome V8引擎的JavaScript运行环境,以其事件驱动、非阻塞I/O的特性在构建高效、轻量级的Web应用和服务API方面表现出色。由于其高效的开发效率和易扩展性,Node.js...

    JS 内存溢出,调试工具介绍

    - **第三方库**:如LeakCanary、memwatch-next等可以帮助检测JavaScript内存泄漏。 总之,理解和掌握JS内存管理及调试工具是解决内存溢出问题的关键。通过定期检查和优化代码,可以有效地预防和解决内存泄漏,提高...

    javascript 数组内存释放

    JavaScript使用了一种称为垃圾回收(Garbage Collection, GC)的自动内存管理系统,它负责检测并释放不再使用的内存。当一个变量引用的对象没有其他任何引用指向它时,GC会认为这个对象是“可回收”的,进而释放其...

    Node.js中内存泄漏分析

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

    本文主要介绍了JavaScript几种常见的内存泄露

    然而,由于其特定的运行环境和语法特性,JavaScript中依然存在一些可能导致内存泄露的情况。本文将深入探讨JavaScript中的几种常见内存泄露模式,并提供相应的解决方案。 1. **全局变量和闭包引用** 全局变量生命...

    JS常见内存泄漏及解决方案解析

    标题“JS常见内存泄漏及解决方案解析”主要讲述了JavaScript中内存泄漏的类型和各种解决方案,以便读者能够在实际开发中避免此类问题,提高程序的性能和稳定性。内存泄漏是指程序中分配的内存由于错误或疏忽未被适当...

    javascript内存浅析

    垃圾回收(Garbage Collection, GC)是JavaScript内存管理的关键部分。当一个对象不再被任何变量引用时,GC会自动清理这个对象所占用的堆内存,以防止内存泄漏。JavaScript有多种GC策略,例如标记清除(Mark-and-...

    JavaMemoryLeak内存泄露.doc

    实际上,Java中的内存泄漏与C++有所不同,主要体现在对象可达性和无用性上。 Java对象的生命周期始于在堆(Heap)上分配,通常通过`new`关键字或反射机制创建。垃圾收集器会监控每个对象的状态,以便在对象不再被...

    nodeJs内存泄漏问题详解

    Node.js是基于Google的V8 JavaScript引擎构建的,因此它的内存管理机制与V8紧密相关。在浏览器环境中,JavaScript的内存泄漏可能不太明显,但在Node.js中,由于长时间运行和处理大量数据,内存泄漏可能导致服务器...

Global site tag (gtag.js) - Google Analytics