FED实验室 - 专注WEB端开发和用户体验

深入javascript(三):垃圾回收机制

JAVASCRIPT 煦涵 3147℃ 0评论

javascript和OOP语言(C#,java)一样也有自己的垃圾回收机制,它的执行环境会管理代码执行过程中使用的内存,这样对于开发者来说,编写javascript程序时不用去考虑内存分配,和内存回收相关问题了,JavaScript垃圾回收的机制很简单:找出不再使用的变量,然后释放掉其占用的内存,但是这个过程不是时时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行这一操作。
那么何为“不在使用的变量”,我们知道javascript中的变量无非就局部变量和全局变量,下面来分析下局部变量的生命周期,局部变量只在函数执行的过程中存在,在执行的过程中,在栈(堆)会为局部变量分配空间,以便存储它们的值,函数执行过程中可以使用它们,函数执行结束,回收局部变量,释放内存,貌似很简单的工作,为什么会有很大开销呢?这仅仅是垃圾回收的冰山一角,就像刚刚提到的闭包,貌似函数结束了,其实还没有,垃圾回收器必须知道哪个变量有用,哪个变量没用,对于不再有用的变量打上标记,以备将来回收。用于标记无用变量的策略有很多,常见策略有两种
一、标记清除(Mark and sweep)

这是JavaScript最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”。至于怎么标记有很多种方式,比如特殊位的反转、维护一个列表等,这些并不重要,重要的是使用什么策略,原则上讲不能够释放进入环境的变量所占的内存,它们随时可能会被调用的到。

垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了,因为环境中的变量已经无法访问到这些变量了,然后垃圾回收器相会这些带有标记的变量机器所占空间。

大部分浏览器都是使用这种方式进行垃圾回收,区别在于如何标记及垃圾回收的时间间隔互有不同而已,毋庸置疑,低版本的IE又是鸡肋。

二、引用计数(Reference counting)

在低版本IE中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候,这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值得引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的内存空间。当垃圾收集器下次再运行时,就会释放引用次数为0的值所占用的内存。

看起来也不错的方式,为什么很少有浏览器采用,还会带来内存泄露问题呢?主要是因为这种方式没办法解决循环引用问题。比如对象A有一个属性指向对象B,而对象B也有有一个属性指向对象A,这样相互引用
代码如下:

function test(){
    var objA = {};
    var objB = {};
    objA.pre = objB;
    objB.pre = objA;
}

这样a和b的引用次数都是2,即使在test()执行完成后,两个对象都已经离开环境,在标记清除的策略下是没有问题的,离开环境的就被清除,但是在引用计数策略下不行,因为这两个对象的引用次数仍然是2,不会变成0,所以其占用空间不会被清理,如果这个函数被多次调用,这样就会不断地有空间不会被回收,造成内存泄露。

在IE中虽然JavaScript对象通过标记清除的方式进行垃圾回收,但BOM与DOM对象却是通过引用计数回收垃圾的,也就是说只要涉及BOM及DOM就会出现循环引用问题。看上面的例子,有同学回觉得太弱了,谁会做这样无聊的事情,其实我们是不是就在做
我们知道,IE中有一部分对象并不是原生 JavaScript 对象。例如,其BOM和DOM中的对象就是使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的。而COM对象的垃圾收集机制采用的引用计数策略。因此即使IE的JavaScript引擎是使用标记清除策略来实现的,但JavaScript访问的COM对象依然是基于引用计数策略的。

代码如下:

var div = document.getElementById("mydiv");//DOM对象
var obj = {};//javascript对象
div.ele = obj;
obj.cc  = div;

 

三、如何解决?
最简单的方式就是自己手工解除循环引用,修改上述代码:
代码如下:

var div = document.getElementById("mydiv");//DOM对象
var obj = {};//javascript对象
div.ele = obj;
obj.cc  = div;
div.ele = null;
obj.cc  = null;

 

将变量设置为null,意味着切断变量与之前引用值之间的链接,当垃圾收集器下次运行时就会删除这些值,释放占用的内存空间。

为了解决循环引用产生的问题,IE9把BOM和DOM都转换成了真正的javascript对象。这样就避免了两种垃圾收集算法并存导致的问题,也消除了常见内存泄露现象。

四、性能问题

垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量、4096个对象、64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器频繁的运行。

微软在IE7中做了调整,触发条件不再是固定的,而是动态修改的,初始值和IE6相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作职能了很多。

当然,同C# 、Java一样我们可以手工调用垃圾回收程序,但是由于其消耗大量资源,而且我们手工调用的不会比浏览器判断的准确,所以不推荐手工调用垃圾回收。

下面是「FED实验室」的微信公众号二维码,欢迎扫描关注:

FED实验室

行文不易,如有帮助,欢迎打赏!

赞赏支持or喜欢 (0)or分享 (0)
捐赠共勉
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址