2008年7月12日

Bloglines - javascript内存泄漏

译言-电脑/网络/数码
发现,翻译,阅读中文之外的互联网精华

javascript内存泄漏

By 3P

原文作者:anonymous
原文链接:JScript Memory Leaks
译者:3P

当一个系统不能正确的管理他的内存分配时,这个系统就存在内存泄漏,这对系统来说是一个bug。内存泄漏的现象可以有程序调用失败、执行减慢等。

微软的Internet Explorer就存在一系列的内存泄漏,其中最严重的就算是执行Jscript时产生的泄漏。当一个DOM对象包涵有一个JavaScript对象(例如一个事件处理函数)的引用,同时如果这个JavaScript对象又包涵该DOM对象,那么这个循环引用就形成了。这种结构本质上没有问题。此时,因为该DOM对象和这个事件处理函数并没有别的引用存在,那么垃圾回收器(一种自动的内存资源管理器)本应该把它们都回收点,并内存释放。JavaScript的垃圾回收器能够检测到这种循环引用,并不会对他产生困惑。但是不幸的是,IE DOM的内存并不能被Jscript所管理。他有他自己的内存管理系统,然而这套系统并不知道循环引用,使得一切都变得混乱。这就导致了,当循环引用形成的时候,内存释放工作不能完成,也就是产生了内存泄漏。长时间的内存泄漏就将产生内存的匮乏,使得浏览器因缺乏必要内存而崩溃?

我们可以来演示一下。在第一段程序-question1中,我们将动态创建以10个为一组共计10000个的DOM元素(<span>),创建10个然后删除在创建10个,如此循环。你在运行这段程序时打开Windows任务管理器,就可以观察到页面运行时PF(虚拟内存)使用率一直保持不变。PF使用率的变化可以视为内存分配是否无效的指标。

Question1

<html>
    <head>
        <title>Queue Test 1</title>
    </head>
    <body>
        <script>
            /*global setTimeout */
            (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);
        </script>
    </body>
</html>

接下来我们运行第二段程序queuetest2。除了做与queuetest1相同的事情以外,它还未每个元素添加了一个点击事件响应函数。在MozilaOpera上,虚拟PF利用率和queuetest1是一样的,但是在IE上我们可以看见由于内存泄漏而产生的每秒一兆的虚拟内存的稳定增量,通常这种泄露都不会被注意到。但是由于Ajax的日益流行,使得页面在浏览器的停留时间增长,使得问题变得常见了。

Question2

<html>

<head><title>Queue Test 2</title>

</head>

<body>

<script>

/*global setTimeout */

(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);

</script>

</body>

</html>

因为IE不能对循环引用进行回收,所以这个任务就落在了我们的肩上。如果我们明确的打破这个循环引用,那么IE就能够完成垃圾回收工作了。具微软的解释,引起内存泄漏的原因是闭包,然而这个结论肯定是非常错误的,并且这使得微软给开发者的建议也成了错误的建议。那么通过DOM来打破循环引用更简单,因为实际上不可能通过Jscript来实现。

当我们处理完一个元素后,我们必须通过把它所有的事件处理函数制空来达到破坏循环引用的目的。我们所需要做的就是把每个事件的处理函数设为空就可以了。我们甚至可以清理函数来完成这一工作。

清理函数将保存一份DOM元素的引用。它将循环检测这个元素的所有属性。如果发现了时间处理函数,就把它值为空。这样就破坏了循环引用,使得内存可以被回收释放。它同样也会检测该元素的子元素,打破他们的循环引用。这个清理函数,只在IE中有效果,对于MozillaOpera都无效。不管是用removeChild()或者是设置innerHTML属性的值,都应该在删除元素之前调用清理函数。

function purge(d) {
    var a = d.attributes, i, l, n;
    if (a) {
        l = a.length;
        for (i = 0; i < l; 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]);
        }
    }
}

那么我们现在来运新第3个程序,queuetest3,在程序3里,元素在被删除之前都调用了清理函数。

Question3

<html>

<head><title>Queue Test 3</title>

</head>

<body>

<p>

Queue Test 3 adds an event handler to each span, and removes it when

finished. See <a href="http://www.crockford.com/javascript/memory/leak.html">http://www.crockford.com/javascript/memory/leak.html</a>

</p>

<script>

/*global onunload, setTimeout */

(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 purge(d) {

var a = d.attributes, i, l, n;

if (a) {

l = a.length;

for (i = 0; i < l; 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]);

}

}

}

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);

}

}

onunload = function (e) {

purge(document.body);

};

loop();

})(10000, 10);

</script>

</body>

</html>

更新:微软发布了该问题的补丁:929874。如果你有十足的信心确保你所有的用户都可以获得该更新,那么你将不再需要上面的清理函数。但不幸的是,这不可能如你所愿,所以可能清理工作在IE6被淘汰之前还是有必要的。

这就是web的天性,有清理不完的bugs