Js中的内存泄露问题


原文链接

Js的内存管理

Js是一种垃圾回收式语言,也就是说,内存是根据对象的创建分配给该对象的,并会在没有该对象的引用时由浏览器回收。Js的垃圾回收机制本身没有问题,但是浏览器在为DOM对象分配和恢复内存的方式上却不尽相同,这就导致了在某些浏览器上内存泄露尤为严重。

IE和FF两个都是罪魁祸首,它们都使用引用计数来为DOM对象处理内存。在引用计数系统中,每个所引用的对象都会保留一个计数,以获悉有多少对象在引用它。如果计数为零,该对象就会被销毁,其占用的内存也会返回给。虽然这种解决方案总的来说还算有效,但在循环引用方面却存在一些缺陷。

循环引用的问题

当两个对象互相引用时,就构成了循环引用,其中每个对象的引用计数值都被赋 1。在纯垃圾回收系统中,循环引用问题不大;但若涉及到的两个对象中的一个对象被任何其他对象引用,那么这两个对象都将被垃圾收集。而在引用计数系统中,这两个对象都不能被销毁,原因是引用计数永远不能为零。

在同是使用了垃圾回收系统(Js语言本身的内存机制,作用对象是Js对象)和引用计数(IE、FF使用的内存机制,作用对象是DOM对象)的混合系统中,将会发生内存泄露,因为系统不能正确识别循环引用。在这种情况下,Js对象和DOM对象均不能被销毁。下述代码显示了Js对象和DOM对象间存在的一个循环引用。

<html>
<body>
<script type="text/javascript">
    document.write("circular references between JavaScript and DOM!");
    var obj;
    window.onload = function() {        
        obj=document.getElementById("DivElement");
        document.getElementById("DivElement").expandoProperty=obj;
        obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
    };
</script>
<div id="DivElement">Div Element</div>
</body>
</html>

// 如上所示,JavaScript 对象 obj 拥有到 DOM 对象的引用,表示为 DivElement。而 DOM 对象则有到此 JavaScript 对象的引用,由 expandoProperty 表示。可见,JavaScript 对象和 DOM 对象间就产生了一个循环引用。由于 DOM 对象是通过引用计数管理的,所以两个对象将都不能销毁。

另一种内存泄露模式

看一下下面的代码,通过调用外部函数 myFunction 创建循环引用。同样,Js对象和DOM对象间的循环引用也会导致内存泄露。

// 由外部函数调用而引起的内存泄露
<html>
<head>
<script type="text/javascript">
    document.write("object references between Js and DOM!");
    function myFunction(elem) {
        this.elemReference = elem;
        // This code forms a circular reference here
        // by DOM --> Js --> DOM
        elem.expandoProperty = this;
    }
    function Leak() {
        // This code will leak
        new myFunction(document.getElementById("myDiv"));
    }
</script>
</head>
<body onload="Leak()">
<div id="myDiv"></div>
</body>
</html>

Js中的闭包

正如上面两个例子所示,循环引用很容易创建,在Js最为方便的编程结构之一:闭包中,循环引用尤为突出。

Js允许函数嵌套,一个嵌套函数可以继承外部函数的参数和变量,而这个嵌套函数则由该外部函数私有。下面看个内部函数的示例:

function parentFunction(paramA) {
    var a = paramA;
    function childFunction() {
        return a + 2;
    }
    return childFunction();
}

Js开发人员使用内部函数来在其他函数中集成小型的实用函数。如上面所示。此内部函数 childFunction 可以访问外部函数 parentFunction 的变量。当内部函数获得和使用其外部函数的变量时,就称其为一个闭包。看下面一个简单的闭包,虽说是简单,但实则很重要:

<html>
<body>
<script type="text/javascript">
    document.write("Closure Demo!!");
    window.onload = function OuterFunc (paramA) {
        var a = paramA;
        return function InnerFunc (paramB) {
            alert(a + "" + paramB);
        };
    };
    var x = OuterFunc("outer x");
    x("inner x");
</script>
</body>
</html>

下面我们对上面这段代码来解剖下看看:

// var x = OuterFunc("outer x"); 这句代码的运行过程:
1. 用 outer x 调用 OuterFunc() ,这样 outer x 通过 outer x --> paramA --> a 这个流程赋值给外部变量(外部函数的本地变量) a 了
2. 然后外部函数执行完步骤1的赋值之后,接着就返回一个指向内部函数的指针,并把这个指针赋给变量 x 了,然后结束本函数的执行
// 外部函数的本地变量 a 即使在外部函数执行结束并返回时仍会存在,这一点不同于 C/C++(一旦函数返回,本地变量也将不复存在)

在上面的外部函数调用时,带有属性 a 的范围对象(一个 AO 对象,具体见11-16的文章)将会被创建,可以理解为是这样的结构:AO.a = paramA ,而这个 paramA 就是上面说的第一步那个传值传进来,所以归根结底是 outer x 。**由于内部函数持有到外部函数的变量 a 的引用,所以这个带属性 a 的范围对象AO将不会被垃圾收集,所以外部函数的本地变量 a 在外部函数执行完毕后仍然存在。

// x("inner x"); 这句代码的执行过程:
1. x 此时是指向内部函数的引用,所以通过给 innner x 的值进行调用时其实是 InnerFunc(inner x) 执行。
2. 所以最后显示:outer x inner x

由上可见,Js闭包功能非常强大,原因是它能使内部函数在外部函数返回时也仍然可以保留对此外部函数的变量的访问。不幸的是,闭包非常易于隐藏 JavaScript 对象 和 DOM 对象间的循环引用。

闭包和循环引用

下面来看一个由事件处理引起的内存泄露模式:

<html>
<body>
<script type="text/javascript">
    document.write("Program to illustrate memory leak via closure");
    window.onload = function outerFunc() {
        var obj = document.getElementById("element");
        obj.onclick = function innerFunc() {
            alert("Hi! I will leak!");
        };

        // The following is used to make the leak significant
        obj.bigString = new Array(1000).join(new Array(2000).join("XXXXX"));
    }
</script>
<button id="element">Click Me to see the Leak</button>
</body>
</html>

虎头蛇尾的文章,后面介绍了三种所谓清除掉内存泄露的方法,没太懂,可能是太深了? : (

左银右煌 /
Published under (CC) BY-NC-SA in categories programming  tagged with JavaScript