study icon indicating copy to clipboard operation
study copied to clipboard

Javascript内存泄露

Open 24wangchen opened this issue 10 years ago • 0 comments

当一个系统无法正确管理它的内存分配时,我们称之为内存泄露。内存泄露是个bug,它会导致系统性能下降甚至挂掉。 IE存在很多漏洞,其中最严重当属js脚本执行。当一个DOM对象引用了一个js对象(事件句柄函数),同时这个js对象又有对该DOM对象的引用,此时就形成了一个循环引用。这本身是没有问题的。如果没有别的对该DOM和js对象的引用,GC将会同时回收它们,释放内存。GC可以识别出循环引用。不幸的是,IE的DOM没有被JS脚本管理。它有自己的一个内存管理,无法识别出循环引用。结果当循环引用存在时,无法进行内存回收。最终这会导致内存耗尽,浏览器崩溃。 下面这段代码将会创建10000个DOM元素,通过队列控制始终保留最近添加的10个元素,旧的被删除掉。打开Windows管理器,该网页所用内存几乎保持稳定。

(function (limit, delay) {
    var queue = new Array(10);
    var n = 0;

    function makeSpan(n) {
        var s = document.createElement('span');
        document.body.appendChild(s);
        var t = document.createTextNode(' ' + n);
        s.appendChild(t);
        return s;
    }

    function process(n) {
        queue.push(makeSpan(n));
        var s = queue.shift();
        if (s) {
            s.parentNode.removeChild(s);
        }
    }

    function loop() {
        if (n < limit) {
            process(n);
            n += 1;
            setTimeout(loop, delay);
        }
    }

    loop();
})(10000, 10);

接下来,运行第二段代码,同样的功能,此时给每一个元素增加一个click事件。在Mozilla和Opera中,内存使用稳定,但在IE中,内存每秒钟都在增长,引发了内存泄露。这个漏洞一般不易察觉。但在Ajax中变得很常见,随着在该页面呆的时间越长,改变的地方越多,那么越容易失败导致崩溃。

(function (limit, delay) {
    var queue = new Array(10);
    var n = 0;

    function makeSpan(n) {
        var s = document.createElement('span');
        document.body.appendChild(s);
        var t = document.createTextNode(' ' + n);
        s.appendChild(t);
        s.onclick = function (e) {
            s.style.backgroundColor = 'red';
            alert(n);
        };
        return s;
    }

    function process(n) {
        queue.push(makeSpan(n));
        var s = queue.shift();
        if (s) {
            s.parentNode.removeChild(s);
        }
    }

    function loop() {
        if (n < limit) {
            process(n);
            n += 1;
            setTimeout(loop, delay);
        }
    }

    loop();
})(10000, 10);

由于IE无法解决循环引用问题,如果我们明确地打破这种循环引用,那么IE将会回收内存。IE的解释是:闭包是引起内存泄露的原因。这种方式当然是错的,但它导致了IE给了开发者错误的处理bug方式。在DOM中解除循环引用很简单,但实际上在JS脚本中不可能解除。 当我们处理完一个元素节点时,我们必须将它的事件句柄置为null,以解决这一问题。 因此我们可以写一个通用的purge函数。 purge函数将DOM节点作为参数。遍历该节点的属性,如果发现是函数,那么置为null,这将解除循环引用,使得内存得以释放。它同时会遍历该节点的后代元素执行此操作。该函数只针对IE起作用,在移除任何节点、removeChild函数或是设置innerHTML属性前执行此函数。

function purge(d) {
    var a = d.attributes, i, l, n;
    if (a) {
        for (i = a.length - 1; i >= 0; i -= 1) {
            n = a[i].name;
            if (typeof d[n] === 'function') {
                d[n] = null;
            }
        }
    }
    a = d.childNodes;
    if (a) {
        l = a.length;
        for (i = 0; i < l; i += 1) {
            purge(d.childNodes[i]);
        }
    }
}

最后来看第三段代码,在删除DOM元素之前调用了purge函数。

(function (limit, delay) {
    var queue = new Array(10);
    var n = 0;

    function makeSpan(n) {
        var s = document.createElement('span');
        document.body.appendChild(s);
        var t = document.createTextNode(' ' + n);
        s.appendChild(t);
        s.onclick = function (e) {
            s.style.backgroundColor = 'red';
            alert(n);
        };
        return s;
    }

    function process(n) {
        queue.push(makeSpan(n));
        var s = queue.shift();
        if (s) {
            purge(s);
            s.parentNode.removeChild(s);
        }
    }

    function loop() {
        if (n < limit) {
            process(n);
            n += 1;
            setTimeout(loop, delay);
        }
    }

    loop();
})(10000, 10);

IE7及以下中存在此问题

GC原理

垃圾回收机制原理其实很简单:找出那些不再使用的变量,然后释放其内存。为此,垃圾收集器会按照一定的触发条件周期性的触发这个过程。垃圾回收最重要的一点就是确定如何确定垃圾对象,实现中主要有两种机制

  • 标记清除 – 由根对象开始标记可到达对象,然后对不可达对象进行回收
  • 引用计数 —维护对象的引用计数,实现上很难避免循环引用带来的困扰

标记清除

  • 当变量进入环境(例如,声明变量)时,这个变量标记为“进入环境”。当变量离开环境时,这将其标记为“离开环境”
  • 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记
  • 去掉环境中变量以及被环境中的变量引用的变量标记。而在此之后仍带有标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了
  • 垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间

IE,Firefox,Opera,Chrome和Safari的目前都是使用的标记清除回收策略

引用计数

  • 跟踪记录每个值被引用的次数。当声明一个变量并将引用类型的值赋给该变量时,则这个值的引用次数就是1
  • 如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得另外一个值,则这个值的引用次数减1
  • 当这个值的引用次数变成0时,则说明没有办法访 问这个值了,因此就可以将其占用的内存空间回收回来。这样当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存

该回收机制会有严重的问题:循环引用 IE中有一部分对象并不是原生的Javascript对象。例如:其BOM和DOM对象就是使用C++以COM对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略。因此,即使,IE的Javascript引擎是使用标记清除策略来实现的,但Javascript访问COM对象依然基于引用计数策略的,这就是上面三段代码所演示的效果,解决方案就是人为地将其置为null,使其计数减一。IE9把BOM和DOM对象转换成了真正的Javascript对象

IE的垃圾回收

IE6垃圾回收 - 根据内存分配量运行的,具体一点说就是256个变量、4096个对象(或数组)和数组元素(slot)或者64KB的字符串。达到上述任何一个临界值,垃圾收集器就会运行。问题在于如果一个脚本中包含那么多 变量,那么该脚本很可能会在其生命中起一直保持那么多的变量,垃圾收集器就可能不得不频繁的运行。

IE7垃圾回收 - IE7中的各项临界值在初始化时与IE6相等。如果例程回收的内存分配量低于15%,则变量 、字面量和(或)数组元素的临界值就会加倍。如果例程回收了85%的内存分配量,则将各种临界重置会默认值。这一看似简单的调整,极大地提升了IE在运行 包含大量JavaScript的页面时的性能。

强制垃圾回收 - 有的浏览器中可以触发垃圾收集过程,在IE中,调用window.CollectGarbage()方法会立即指向垃圾收集。

WebKit的垃圾回收

主要步骤为

  • 标记对象
  • 清除
  • 压缩空间

相关代码在heap.cpp

void Heap::collectAndSweep(HeapOperation collectionType)
{
    if (!m_isSafeToCollect)
        return;
        collect(collectionType);
    SamplingRegion samplingRegion("Garbage Collection: Sweeping");
        DeferGCForAWhile deferGC(*this);
        m_objectSpace.sweep();
        m_objectSpace.shrink(); 
        sweepAllLogicallyEmptyWeakBlocks();
}

V8引擎的垃圾回收

关于V8引擎的GC机制可查看这篇文章英文原文 延伸阅读:

24wangchen avatar Apr 19 '15 15:04 24wangchen