javascript的垃圾回收机制

    和java,c#一样,javascript也有自动垃圾回收的机制,比如说c++和c就没有自动垃圾回收机制。可能有这么一种倾向,垃圾回收机制必须有一种平台来进行回收。比如说下面将的javascript的执行环境V8就会负责管理代码执行过程中的垃圾回收。

    javascript具有自动垃圾回收机制,执行环境会负责管理代码执行过程中使用的内存。原理就是找出那些不再继续使用的变量,然后释放其占有内存。这整个过程也会按照一个固定的事件周期性的整形(时时的话开销太大)。


    变量的声明周期

    刚刚原理中提到要找出不再使用的变量,什么是不再使用的对象呢?不再使用的变量也就是生命周期结束的变量。目前javascript有两种变量,全局变量和在函数中产生的局部变量(暂不考虑ES6中的块级作用域)。

    全局变量的声明周期一直持续到浏览器关闭页面才会清除,而局部变量只是在函数执行器存在,而在这个过程中会为局部变量在栈或者堆上分配相应的空间,来存储他们的值,然后当函数要使用这些变量的值时再取出来使用。一直到函数结束(闭包会不同)。

    一旦函数结束,局部变量就不需要了,这时候就可以释放他们的内存。

    1
    2
    3
    4
    5
    var globalVariable = "I'm global";
    function test(){
    var localVariable = "I'm local";
    }
    test();

    这个例子里面,global在关闭浏览器时释放,local在函数test结束后,释放。

    具体看下垃圾回收的两种回收机制。


    js的两种回收机制

    标记清除(mark and sweep)

    从语义上理解就比较好理解了,大概就是当变量进入到某个环境中的时候就把这个变量标记一下,比如标记为“进入环境”,当离开的时候就把这个变量的标记给清除掉,比如是“离开环境”。而在这后面还有标记的变量将被视为准备删除的变量。

    垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上的标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作。销毁那些带标记的值并回收它们所占用的内存空间。

    这是javascript最常见的垃圾回收方式。至于上面有说道的标记,到底该如何标记?好像是有很多方法,比如特殊位翻转,维护一个列表什么的。


    引用计数(reference counting)

    引用计数的含义是跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个时候的引用类型的值就会是引用次数+1了。如果同一个值又被赋给另外一个变量,则该值的引用次数又+1。

    相反如果包含这个值的引用的变量又取得另外一个值,即被重新赋了值,那么这个值的引用就减一。当这个值的引用次数编程0时,表示没有用到这个值,这个值也无法访问,因此环境就会收回这个值所占用的内存空间回收。这样,当垃圾收集器下次再运行时,它就会释放引用次数为0的值所占用的内存。

    但是刚刚也说了,第一种标记清除是最经常用到的,那么这个看起来这么好的引用计数为啥不被别人用了呢?

    因为这个过程中会出现一个循环引用的问题!

    简单点来说就是一个对象小a的属性,引用了对象小b。小b对象也有一个属性引用了小a,那么小a,小b互相引用对方,也就造成了循环引用的问题啦。

    煮个栗子:

    1
    2
    3
    4
    5
    6
    function test(){
    var a = {};
    var b = {};
    a.property = b;
    b.property = a;
    }

    这就是一个很明显的循环引用了,小a和小b通过各自的属性互相引用,导致了内存无法释放。(有那么一点点的感觉像死锁。。。。)即使是再test()执行完后,如果使用标记清除是没有问题的,离开环境的时候就会被清除。但是引用计数不行,因为这两个对象的引用次数还是存在,不会变成0,所以其占用空间也不会清理,如果这个函数被调用多次,就会不断有内存被占用。造成了内存泄露。

    IE中有一部分对象并不是原生JavaScript对象。例如,其BOM和DOM中的对象就是使用C++以COM(Component Object Model)对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略。

    因此即使IE的js引擎是用的标记清除来实现的,但是js访问COM对象如BOM,DOM还是基于引用计数的策略的,也就是说只要在IE中设计到COM对象,也就会存在循环引用的问题。

    比如说第一种情况:一个DOM元素和一个原生的js对象之间的循环引用

    1
    2
    3
    4
    5
    6
    7
    var ele = document.getElementById("ele");
    var obj = {};
    obj.property = ele;
    ele.property = obj;
    //这种情况应该手动设置,在不适用的时候手工断开js和dom元素之间的连接
    obj.property = null;
    ele.property = null;

    比如第二种情况是闭包的作用域链中包含着一个html元素,那么这个元素无法被销毁

    1
    2
    3
    4
    5
    6
    window.onload = function outerFunction(){
    var ele= document.getElementById("element");
    ele.onclick = function (){
    console.log(ele.id);
    }
    }

    上面这个代码创建了一个作为ele元素处理程序的闭包,而这个闭包则又创建了一个循环引用。y匿名函数中保存了一个outerFunction()的活动对象的引用,因此就会导致无法减少ele的引用。可以改成下面这个:

    1
    2
    3
    4
    5
    6
    7
    8
    window.onload = function outerFunction(){
    var ele= document.getElementById("element");
    var id = ele.id;
    ele.onclick = function (){
    console.log(id);
    }
    ele = null;
    }

    在上面的代码中,通过把ele.id 的一个副本保存在一个变量中,并且在闭包中引用改变量消除了循环引用。

    必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着elem。即使闭包不直接引用ele(比如上面的例子我们不用id),包含函数的多动对象中也依旧会保存一个引用。因此,有必要把ele变量设置为null。这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

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

    为了解决上述问题,IE9把BOM和DOM对象都转换成了真正的JavaScript对象。


    javascript与V8引擎

    垃圾回收机制的好处和坏处

    好处:大幅简化程序的内存管理代码,减轻程序猿负担,并且减少因为长时间运转而带来的内存泄露问题。

    坏处:自动回收意味着程序猿无法掌控内存。ECMAScript中没有暴露垃圾回收的借口,我们无法强迫其进行垃圾回收,更加无法干预内存管理。


    node内存管理问题

    在浏览器中,V8引擎实例的生命周期不会很长(因为我们使用完网站就会把网站关闭),而且运行在用户的机器上。如果不幸发生内存泄露等问题,仅仅会影响到一个终端用户。且无论这个V8实例占用了多少内存,最终在关闭页面时内存都会被释放,几乎没有太多管理的必要(当然并不代表一些大型Web应用不需要管理内存)。但如果使用Node作为服务器,就需要关注内存问题了,一旦内存发生泄漏,久而久之整个服务将会瘫痪(服务器不会频繁的重启)。


    涨知识之V8内存限制

    Node与其他语言不同的一个地方,就是其限制了JavaScript所能使用的内存(64位为1.4GB,32位为0.7GB),这也就意味着将无法直接操作一些大内存对象。这很令人匪夷所思,因为很少有其他语言会限制内存的使用。

    V8之所以限制了内存的大小,表面上的原因是V8最初是作为浏览器的JavaScript引擎而设计,不太可能遇到大量内存的场景,而深层次的原因则是由于V8的垃圾回收机制的限制。由于V8需要保证JavaScript应用逻辑与垃圾回收器所看到的不一样,V8在执行垃圾回收时会阻塞JavaScript应用逻辑,直到垃圾回收结束再重新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。若V8的堆内存为1.5GB,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收甚至要1秒以上。这样浏览器将在1s内失去对用户的响应,造成假死现象。如果有动画效果的话,动画的展现也将显著受到影响。


    总结

    对js的垃圾回收机制有所了解了,也明白了平常自己写代码的时候肯能会有这些问题,比如COM的循环引用。但是看到IE9的已经把COM变成了真正的js对象,以后应该会越来越好。主要学习和整理自:segmentfault和javascript的高级程序设计。感谢。