性能调优之Javascript内存泄漏

1.什么是内存泄漏?

内存泄漏是指分配给应用的内存不能被重新分配,即使在内存已经不被使用的时候。正常情况下,垃圾回收器在DOM元素和event处理器不被引用或访问的时候回收它们。但是,IE的早些版本(IE7和之前)中内存泄漏是很容易出现的,因为内存管理器不能正确理解Javascript生命周期而且在周期被打破(可以通过赋值为null实现)前不会回收内存。

2.为什么你需要注意它?

在大型Web应用程序中内存泄漏是一种常见的意外编程错误。内存泄漏会降低Web应用程序的性能,直到浪费的内存超过了系统所能分配的,应用程序将不能使用。作为一Web开发者,开发一个满足功能要求的应用程序只是第一步,性能要求和Web应用程序的成功是同样重要的,更何况它可能会导致应用程序错误或浏览器崩溃。

3.Javascript中出现内存泄漏的主要原因是什么?

1)循环引用

一个很简单的例子:一个DOM对象被一个Javascript对象引用,与此同时又引用同一个或其它的Javascript对象,这个DOM对象可能会引发内存泄漏。这个DOM对象的引用将不会在脚本停止的时候被垃圾回收器回收。要想破坏循环引用,引用DOM元素的对象或DOM对象的引用需要被赋值为null。

2)Javascript闭包

因为Javascript范围的限制,许多实现依赖Javascript不包,请查看我的前面的文章JavaScript Scope and Closure如果你想了解更多闭包方面的问题。

闭包可以导致内存泄漏是因为内部方法保持一个对外部方法变量的引用,所以尽管方法返回了内部方法还可以继续访问在外部方法中定义的私有变量。对Javascript程序员来说最好的做法是在页面重载前断开所有的事件处理器。

3)DOM插入顺序

当2个不同范围的 DOM 对象连添加到一起的时候一个临时的对象会被创建。这个DOM对象改变范围到document时,那个临时对象就没用了。也就是说, DOM 对象应该按照从当前页面存在的最上面的 DOM 元素开始往下直到剩下的 DOM 元素的顺序添加,这样它们就总是有同样的范围,不会产生临时对象。

4)如何检测?

内存泄漏对开发者来说一般很难检测因为它们是由一些大量代码中的意外的错误引起的,但它在系统内存不足前并不影响程序的功能。这就是为什么会有人在很长时间的测试期中收集应用程序性能指标来测试性能。

最简单的检测内存泄漏的方式是用任务管理器检查内存使用情况。在Chrome浏览器的新选项卡中打开应用并查看内存使用量是不是越来越多。还有其他的调试工具提供内存监视器,比如Chrome开发者工具。这是谷歌开者这网站中的堆分析的特性的教程。

什么是内存泄露

内存泄露是指一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束。在C++中,因为是手动管理内存,内存泄露是经常出现的事情。而现在流行的C#和Java等语言采用了自动垃圾回收方法管理内存,正常使用的情况下几乎不会发生内存泄露。浏览器中也是采用自动垃圾回收方法管理内存,但由于浏览器垃圾回收方法有bug,会产生内存泄露。

内存泄露Quick View

不同的浏览器中存在各种内存泄露方式,目前发现的主要是这样几种:

1. 循环引用

已经确认存在泄漏的浏览器:IE6.0 FF2.0

含有DOM对象的循环引用将导致大部分当前主流浏览器内存泄露 这里有两个简单的概念

引用:a.属性=b,a就引用了b

循环引用:简单来说假如a引用了b,b又引用了a,a和b就构成了循环引用。

a和b循环引用:

var a=new Object;

var b=new Object;

a.r=b;

b.r=a;

a循环引用自己:

var a=new Object;

a.r=a;

循环引用很常见且大部分情况下是无害的,但当参与循环引用的对象中有DOM对象或者ActiveX对象时,循环引用将导致内存泄露。我们把例子中的任何一个new Object替换成document.getElementById或者document.createElement就会发生内存泄露了。

尽管这看起来非常容易理解,但是因为有closure的参与而使事情变得复杂,有些closure导致的循环引用很难被察觉。下面是一个非常常见的动态绑定事件:

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    obj.onclick=function(){ 
        //Even if it's a empty function 
    } 
}

这个bindEvent执行时100%会发生内存泄露,Someone 可能会问,哪里出现了循环引用? 关于closure和scope chain参与的循环引用比较复杂,此处暂不深入讨论。有一个简单的判断方式:函数将间接引用所有它能访问的对象。obj.onclick这个函数中 可以访问外部的变量obj 所以他引用了obj,而obj又引用了它,因此这个事件绑定将会造成内存泄露。在IBM的文章中介绍了2种方式解决类似的问题一个是obj=null,另一个是把onclick的函数写在bindEvent外,重复人家的我就不说了。简单贴下代码:

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    obj.onclick=onclickHandler; 
} 
function onclickHandler(){ 
    //do something 
}

 

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    obj.onclick=function(){ 
        //Even if it's a empty function 
    } 
    obj=null; 
}

这两个方法都打断了循环引用,可以解决问题,但是似乎对代码表达能力造成了一定破坏,假设有这么一个问题:

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    var var0="OOXX";//Here is a variable 
    obj.onclick=function(){ 
        alert(var0);//I want to visit var2 here! 
    } 
    return obj;//bindEvent must return obj! 
}

好了,这下两种办法都不行了,假如我把函数写外面去,var0肯定访问不了,假如我把obj弄成null,还怎么return它呢?这并不是空想的需要,这实际 上是一个用JS定制DOM控件的简单抽象:创建DOM元素、设置私有属性、绑定事件。所以,我们必须update一下两个方法。首先,方法1,为了让函数 能访问某些变量,我们可以通过一个Builder函数来订制onclick的外部闭包:

function bindEvent() 
{ 
    var obj=document.createElement("XXX"); 
    var var0="OOXX";//Here is a variable 
    obj.onclick= onclickBuilder(var0);//想访问谁就把谁传进去!! 
    return obj;//bindEvent must return obj! 
} 
function onclickBuilder(var0)//这里跟上面对应上就行了 最好参数名字也对应上 
{ 
    return function(){ 
        alert(var0); 
    } 
}

第二个办法,这个来自51js的chpn同学,让obj=null在return 之后执行!!

function bindEvent() 
{ 
    try{ 
        var obj=document.createElement("XXX"); 
        var var0="OOXX";//Here is a variable 
        obj.onclick=function(){ 
            alert(var0);//I want to visit var2 here! 
        } 
        return obj;//bindEvent must return obj! 
    } finally { 
        obj=null; 
    } 
}

2. 某些DOM操作

这是IE系列的特有问题 简单的来说就是在向不在DOM树上的DOM元素appendChild,可能会发生内存泄露(只是可能,具体原因不明,似乎十分复杂,下面例子中去掉onClick也可以避免泄露)。所以appendChild的顺序可能影响内存泄露,来自微软的例子:

<html> 
    <head>  
        <script language="JScript"> 
        function LeakMemory()  
        { 
            var hostElement = document.getElementById("hostElement");  
            // Do it a lot, look at Task Manager for memory response  
            for(i = 0; i < 5000; i++)  
            {  
                var parentDiv =  
                    document.createElement("<div onClick='foo()'>");  
                var childDiv = 
                    document.createElement("<div onClick='foo()'>"); 
                // This will leak a temporary object  
                parentDiv.appendChild(childDiv);  
                hostElement.appendChild(parentDiv);  
                hostElement.removeChild(parentDiv); 
                parentDiv.removeChild(childDiv); 
                parentDiv = null; 
                childDiv = null; 
            } 
            hostElement = null; 
        } 
        function CleanMemory() 
        { 
            var hostElement = document.getElementById("hostElement"); 
            // Do it a lot, look at Task Manager for memory response 
            for(i = 0; i < 5000; i++) 
            { 
                var parentDiv = 
                    document.createElement("<div onClick='foo()'>"); 
                var childDiv = 
                    document.createElement("<div onClick='foo()'>"); 
                // Changing the order is important, this won't leak 
                hostElement.appendChild(parentDiv); 
               parentDiv.appendChild(childDiv); 
                hostElement.removeChild(parentDiv); 
                parentDiv.removeChild(childDiv); 
                parentDiv = null; 
                childDiv = null; 
            } 
            hostElement = null; 
        } 
        </script> 
    </head> 
    <body> 
        <button onclick="LeakMemory()">Memory Leaking Insert</button> 
        <button onclick="CleanMemory()">Clean Insert</button> 
        <div id="hostElement"></div> 
    </body> 
</html>

而在IE7中,貌似为了改善内存泄露,IE7采用了极端的解决方案:离开页面时回收所有DOM树上的元素,其它一概不管。但是这不仅没起到任何作用,反而 使问题变得更加复杂。对这类问题,除了自觉一点绕开这些恶心的东西,多用innerHTML这种无用的建议之外。我想可以通过覆盖 document.createElement来略为改善:

首先我们定义一个看不见的元素当作垃圾箱,所有新创建的元素都扔进垃圾箱里,这样保证了所有DOM元素都在DOM树上,IE7就可以正确回收了,另一方面也能避免所谓的”appendChild顺序不对导致内存泄露”。

function MemoryFix(){ 
    var garbageBox=document.createElement("div"); 
    garbageBox.style.display="none"; 
    document.body.appendChild(garbageBox); 
    var createElement=document.createElement; 
    document.createElement=function(){ 
        var obj=Function.prototype.apply.apply(createElement,[document,arguments]); 
        garbageBox.appendChild(obj); 
        return obj; 
    } 
}

3. 自动类型装箱转换

别不相信,下面的代码在ie系列中会导致内存泄露

var s=”lalala”;

alert(s.length);

s本身是一个string而非object,它没有length属性,所以当访问length时,JS引擎会自动创建一个临时String对象封装s,而这个对象一定会泄露。这个bug匪夷所思,所幸解决起来相当容易,记得所有值类型做.运算之前先显式转换一下:

var s=”lalala”;

alert(new String(s).length);

4) Timers计(定)时器泄露

定时器也是常见产生内存泄露的地方:
for (var i = 0; i < 90000; i++) {
  var buggyObject = {
    callAgain: function() {
      var ref = this;
      var val = setTimeout(function() {
        ref.callAgain();
      }, 90000);
    }
  }

  buggyObject.callAgain();
  //虽然你想回收但是timer还在
  buggyObject = null;
}


关注我

我的微信公众号:前端开发博客,在后台回复以下关键字可以获取资源。

  • 回复「小抄」,领取Vue、JavaScript 和 WebComponent 小抄 PDF
  • 回复「Vue脑图」获取 Vue 相关脑图
  • 回复「思维图」获取 JavaScript 相关思维图
  • 回复「简历」获取简历制作建议
  • 回复「简历模板」获取精选的简历模板
  • 回复「加群」进入500人前端精英群
  • 回复「电子书」下载我整理的大量前端资源,含面试、Vue实战项目、CSS和JavaScript电子书等。
  • 回复「知识点」下载高清JavaScript知识点图谱

每日分享有用的前端开发知识,加我微信:caibaojian89 交流