魔鬼的梦魇—验证IE中的JS内存泄露模式(一)

随着移动互联网的发展,现在越来越多的应用开始想bs方向转移,原来很多复杂的计算逻辑也自然随着转移到了客户端,需要客户端的javascript来担任实现的角色;原本不起眼的内存泄露,现在却犹如恶魔会很快的吞噬掉用户的内存。从去年开始就在专注js面向对象开发实现、托管代码垃圾回收算法、js内存泄露相关的东西;如果大家关注过这方面的东西,那我想你一定读过微软开发人员Justin Rogers撰写的那篇经典的内存泄露模式,其实道理阐述的很清楚,但是怎么证明和呈现泄露的存在呢?这是一直困扰我的一个难题!今天我们针对这篇文章的各个模式,记录一下自己的一些想法,很多是自己的推论,并不一定正确,欢迎大家批评指正;

泄露的客观因素

在过去,对于web开发人员内存泄露并没有引起大的问题,主要是因为页面都相对比较简单,并且页面也只是在一个站点内跳转(这也是一种很好的释放松散内存的方式);如果有内存泄露的话,那也是小的不足以引起开发人员的注意。

随着技术的发展,新的应用程序需要遵循更高的标准。一个页面或许会运行数个小时而没有跳转页面并通过web服务动态检索数据进行更新。复杂的事件机制、面向对象开发、闭包等很多语言特性被用来开发整个应用程序。在这些和其他的一些改变下,某些内存泄露模式开始变得更加突出,尤其是那些以前被页面跳转隐藏的泄露问题。

内存泄露模式

接下来我们将会讨论内存泄露模式,并且每个模式都会给出一些简单的例子。现在,我们简单的看一下以下的模式:

1. 循环引用---当IE中COM对象和js的对象形成循环引用的时候,对象得不到释放就是造成内存泄露。这是一种最广泛的模式。

2. 闭包---作为一种在web应用程序中使用最广泛的模式,闭包是以一种特殊方式的循环引用造成内存泄露的。

3. 跨页泄露---当你从一个页面跳转到另一个页面的时候,由于产生的一些中间对象会形成很微弱的内存泄露。

4. Pseudo-Leaks---这个并不是真正的泄露,但是如果你不理解这个,那么你的内存不断增加也是十分恼人的。

以上是根据自己对文章的理解,对部分文字的翻译,下面我们将以同样的方式,先就翻译,然后记录相应的思考。

循环引用模式

循环引用几乎是所有泄露的根源。正常情况下,js的脚本引擎是可以通过垃圾回收器解

决循环引用问题的(js的垃圾回收器使用的标记擦除法,前面已经介绍过标记擦除法,这种算法本身是可以解决循环引用的问题),但是某些未知的情况会阻止其正常工作。在IE中这些未知情况出现在js对象可以访问DOM元素的情况下。其基本的规则原理如下图(循环引用模式原理图)所示

该模式导致内存泄露的原因是基于COM的引用计数器。Js引擎引用一个DOM元素,那么回收和释放DOM元素的引用,只能一直等待到对其引用被解除。在我们的图中,js引擎存在两个引用:scope对象和DOM元素的expando属性。当js引擎结束的时候将会释放第一个引用,但是对DOM元素的引用并不会释放,因为它会一直等待js引擎释放对它的引用。你或许会想很容易就可以探测到这种情况并解决这个问题,但是在实践中这只是众多情况中的冰山一角。如果循环引用是由30个对象的环(类似链表)形成的,那么将会很难探测到。


图 1. 循环引用模式原理图

根据这个原理图,我写了个测试的例子,在函数principlePictureLeak中,我们获取到span元素,并使obj1指向它,然后实例化对象obj4,并使span元素的expando属性指向obj4,具体的代码如下

//循环引用测试工具类
var CircularReferencesTester =
{
    //span元素id
    spanTagId: 'principlePictureSpan'
    ,
    //循环引用原理图的泄露例子
    principlePictureLeak: function () {
        var spanElement = document.getElementById(CircularReferencesTester.spanTagId);
        var obj1 = spanElement;
        var obj4 = {};        
        spanElement.expando = obj4;
    }
    ,
    //绑定事件
    attachEvent: function (element, eventName, handler) {
        element.attachEvent("on" + eventName, handler);
    }
    ,
    //页面加载完后触发的动作
    ready: function (handle) {
        CircularReferencesTester.attachEvent(window, "load", handle);
    }
    ,
    //初始化
    init: function () {
        CircularReferencesTester.ready(CircularReferencesTester.principlePictureLeak);
    }
};


CircularReferencesTester.init();


 
 

 
 
 

在内存泄露检测工具sIEve中运行以上代码,我们可以看到并没有内存泄露,具体信息详见 下图(循环引用原理图内存泄露检测图)

图 2. 循环引用原理图内存泄露检测图

那么为什么没有形成内存泄露呢?按照Justin Rogers对循环引用模式原理的解释,上边的图1并没有完全展示出其描述的对象关联关系,那么我们可以看一下这些对象之间的关系(如图3),从图中我们可以看到对象图中并没有形成真正的”圆环”,自然也就没有形成真的循环引用,在函数执行完以后,js引擎就会释放对方法scope对象的引用,然后scope对象就会释放对obj1、obj4的引用,然后obj1就会释放对DOM元素的引用;那么这个时候如果执行完函数后,obj4是否会释放内存呢?


图 3. 循环引用模式原理图的对象关系

那么我们先来看以下的代码测试,其意图就是在setReference里使span元素引用一个js的对象,然后在getReference里来获取这个对象并输出其属性msg的值,经测试其最终输出了“DOM引用了js对象”,那么我们可以知道,DOM应用的局部变量并不会因为函数执行完毕而被回收掉。

var DOMReleaseReferenceTestor = { //span元素id spanTagId: 'principlePictureSpan' , //设置span元素引用js的字符串 setReference: function () { document.getElementById(DOMReleaseReferenceTestor.spanTagId).obj ={msg : 'DOM引用了js对象'}; } , //获取设置的span元素的字符串 getReference: function () { alert(document.getElementById(DOMReleaseReferenceTestor.spanTagId).obj.msg); } , //初始化 init: function () { window.attachEvent("onload", DOMReleaseReferenceTestor.getReference); window.attachEvent("onload", DOMReleaseReferenceTestor.setReference); } }; DOMReleaseReferenceTestor.init();

通过以上的代码测试,principlePictureLeak执行完成后,并没有回收obj4对象,但是当我们在sIEve中重复刷新页面并没有泄露,那么也就证明了,在刷新页面的时候,js脚本引擎解除了对DOM的引用,当DOM被回收后,就会释放对obj4的引用,从而成功的回收obj4。

以下是Justin Rogers针对循环引用模式给出的一个例子,代码如下

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <script language="JScript"> var myGlobalObject ; function SetupLeak() { // First set up the script scope to element reference myGlobalObject = document.getElementById("LeakedDiv"); // Next set up the element to script scope reference document.getElementById("LeakedDiv").expandoProperty = myGlobalObject; } function BreakLeak() { document.getElementById("LeakedDiv").expandoProperty =null; } </script> </head> <body οnlοad="SetupLeak()" οnunlοad="BreakLeak()"> <div id="LeakedDiv"></div> </body> </html>

我们可以简单的看以下各对象之间的引用关系(如图4),从图中我们可以看到myGlobalObject和LeakedDiv之间形成了循环引用的”环路”,这个在理论上构成了内存泄露的条件,那么到底有没有泄露呢,让我们使用sIEve来测试一下吧。

图 4. 循环引用例子1对象关系图

测试结果如下图(图5),让我们跌破眼睛的事情发生了,我们期盼已久的内存泄露并没有出现。那么这又是什么原因呢?那么下边我们看一下我这个可以出现内存泄露的代码例子。从代码中我们可以看到唯一不同的地方,就是我们新增的DOM元素,而不是直接使用的页面中的DOM元素。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> </head> <script type="text/javascript"> var myGlobalObject; var CircularReferencesTester = { leak: function () { this._leak(true); } , unLeak:function(){ this._leak(false); } , _leak:function(leak){ myGlobalObject = document.createElement("<div />"); if (leak) { myGlobalObject.expandoProperty = myGlobalObject; } } }; CircularReferencesTester.leak(); </script> <body> </body> </html>

图 5. 循环引用例子泄露测试结果

先来看一下sIEve的测试结果吧,如图6,从图中我们可以看到,随着页面的刷新,内存泄露的数目不断增加,通过对比,我们可以知道循环引用模式产生内存泄露除了满足循环引用外,其中的DOM是动态新增的,而不是页面原有的DOM!

图 6. 我的循环引用例子泄露测试结果

最后总结一下,循环引用模式产生内存泄露除了满足循环引用外,其中的DOM是动态新增的,而不是页面原有的DOM!测试环境window 7 + IE 8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值