UDN-企业互联网技术人气社区

板块导航

浏览  : 801
回复  : 11

[原生js] GC与JS内存泄露

[复制链接]
genie1003的头像 楼主
发表于 2017-2-9 10:51:49 | 显示全部楼层 |阅读模式
  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种,一是清楚后对内存进行复制整理,就像磁盘整理程序那样,把所有还在使用的内存移到一起,把释放掉的内存移到一起,如图:
1.png


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

  第二种方案是不移动内存,而是按大小分类,建立一系链表,把这些碎片按大小连接并管理起来,(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.png

  解决方案很简单,在确保属性不再使用后,加入以下代码就可以了:
  1. function  BreakLeak(){  // 解开循环引用,解决内存泄露问题
  2.           document.getElementById( " LeakedDiv " ).expandoProperty  =  null ;
  3. }
复制代码

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

相关帖子

发表于 2017-2-9 10:52:23 | 显示全部楼层
前排支持下
使用道具 举报

回复

发表于 2017-2-12 00:42:34 来自手机 | 显示全部楼层
有空一起交流一下
使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部