1.Javascript的开发习惯与准则
2.Javascript模式
3.
什么是循环引用?首先搞清楚什么是引用,一个对象A的属性被赋值为另一个对象B时,则可以称A引用了B。假如B也引用了A,那么A和B之间构成了循环引用。同样道理 如果能找到A引用B B引用CC又引用A这样一组饮用关系,那么这三个对象构成了循环引用。当一个对象引用自己时,它自己形成了循环引用。注意,在js中变量永远是对象的属性,它可以指向对象,但决不是对象本身。
循环引用很常见,而且通常是无害的,但如果循环引用中包含DOM对象或者ActiveX对象,那么就会发生内存泄露。例子:
var a=document.createElement("div");
var b=new Object();
a.b=b;
b.a=a;
Javascript引擎和DOM采用的垃圾回收算法:引用计数javascript和DOM有各自的垃圾回收器,单独运作良好,合作时一不小心会出问题。引用计数这个算法的缺陷就是:Javascript 对象和DOM对象彼此循环引用,造成彼此的引用计数永远不能为0,垃圾回收器无法正确回收这些参与循环引用的对象,最终造成内存泄漏(Memory Leak)。闭包是循环引用“大户”。如果对垃圾回收感兴趣,可以看看
在编写Javascript程序时,开发人员不用关心内存问题,内存分配及无用内存的回收完全实现了自动化管理。垃圾收集器会按照预定的时间间隔,周期性的找出那些不再继续使用的变量,然后释放其所占用的内存。具体到浏览器中,用于标识无用变量的策略,通常有两种:标记清除和引用策略。
标记清除
标记清除策略是Javascript中最常用的垃圾收集方式,截止2008年,IE、Firefox、Opera、Chrome和Safari采用的都是标记清除方式或者类似的策略,只不过垃圾收集的间隔时间有所不同。
标记清除的原理很容易理解,当变量进入一个执行环境时,将这个变量标记为“进入环境”,当变量离开环境时,则将其标记为“离开环境”。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后它会去掉当前环境中的变量以及被环境中的变量引用的变量的标记,而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问这些变量了。最后,垃圾收集器完成内存清除的工作,销毁那些带标记的值并回收他们占据的内存空间。
引用计数
引用计数的含义是跟踪记录每个值被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数便是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1,相反,如果包含对这个值引用的变量又取得了另一个值,则这个值的引用次数减1。当这个值的引用次数为0时,说明没有办法访问到它了,因而可以将其占用的内存空间回收。
引用计数策略遭遇的一个严重问题是循环引用,请看下面的例子:
1 2 3 4 5 6 | function memoryProblem () { var o1 = new Object(); var o2 = new Object(); o1.prop = o2; o2.prop = o1; } |
上面的代码中,当函数memoryProblem执行完毕后,o1和o2的引用次数不为0,最后得不到垃圾收集器的内存回收。
内存泄漏
当前主流浏览器的垃圾回收均采用标记清除方式来进行管理,但有一个非常特殊并且我们不得不面对的就是IE浏览器的对象并不全是原生的 Javascript对象,其BOM和DOM对象是使用C++以COM对象的形式实现的,这部分对象的内存管理相对于Javascript原生对象是独立的。问题的出现正是因为COM对象的内存回收是采用引用计数策略而非标记清除方式。
如果我们定义的Javascript对象与DOM或BOM对象之间形成循环引用,在IE下则会发生内存不能被正确回收的问题,这也是最常见的IE内存泄漏。如下面的例子:
1 2 3 4 | var elem = document.getElementById( 'elemId' ); var o = new Object(); o.prop = elem; elem.attr = o; |
上面的代码中,DOM对象elem与Javascript对象o之间产生了循环引用,即使将elem从页面中移除(removeChild或者replaceChild),其所占用的内存也不会被回收。
解决办法就是,在不使用这些变量时手工解除Javascript对象和DOM对象的引用,比如:
1 2 | o.prop = null ; elem.attr = null ; |
一.内存管理
如果循环引用中包含 DOM 对象或者 ActiveX 对象,那么就会发生内存泄露。内存泄露的后果是在浏览器关闭前,即使是刷新页面,这部分内存不会被浏览器释放。
通常循环引用发生在为 dom 元素添加闭包作为 expendo 的时候
如:
- function init() {
- var el = document.getElementById('MyElement');
- el.onclick = function () {……}
- }
- init();
init 在执行的时候,当前上下文我们叫做 context 。这个时候, context 引用了 el , el 引用了 function , function 引用了 context 。这时候形成了一个循环引用。
function elClickHandler() {……}
function init() {
var el = document.getElementById('MyElement');
el.onclick = elClickHandler;
}
init();
把 function 抽到新的 context 中,这样, function 的 context 就不包含对 el 的引用,从而打断循环引用。
知道了这些不符合规范的代码解析bug以后,我们如果用它的话,就会发现内存方面其实是有问题的,来看一个例子:
1 | var f = ( function (){ |
2 | if ( true ) { |
3 | return function g(){}; |
4 | } |
5 | return function g(){}; |
6 | })(); |
我们知道,这个匿名函数调用返回的函数(带有标识符g的函数),然后赋值给了外部的f。我们也知道,命名函数表达式会导致产生多余的函数对象,而该对象与返回的函数对象不是一回事。所以这个多余的g函数就死在了返回函数的闭包中了,因此内存问题就出现了。这是因为if语句内部的函数与g是在同一个作用域中被声明的。这种情况下 ,除非我们显式断开对g函数的引用,否则它一直占着内存不放。
01 | var f = ( function (){ |
02 | var f, g; |
03 | if ( true ) { |
04 | f = function g(){}; |
05 | } |
06 | else { |
07 | f = function g(){}; |
08 | } |
09 | // 设置g为null以后它就不会再占内存了 |
10 | g = null ; |
11 | return f; |
12 | })(); |
通过设置g为null,垃圾回收器就把g引用的那个隐式函数给回收掉了,为了验证我们的代码,我们来做一些测试,以确保我们的内存被回收了。
测试
测试很简单,就是命名函数表达式创建10000个函数,然后把它们保存在一个数组中。等一会儿以后再看这些函数到底占用了多少内存。然后,再断开这些引用并重复这一过程。下面是测试代码:
01 | function createFn(){ |
02 | return ( function (){ |
03 | var f; |
04 | if ( true ) { |
05 | f = function F(){ |
06 | return 'standard' ; |
07 | }; |
08 | } |
09 | else if ( false ) { |
10 | f = function F(){ |
11 | return 'alternative' ; |
12 | }; |
13 | } |
14 | else { |
15 | f = function F(){ |
16 | return 'fallback' ; |
17 | }; |
18 | } |
19 | // var F = null; |
20 | return f; |
21 | })(); |
22 | } |
23 | var arr = [ ]; |
24 | for ( var i=0; i < 10000; i++) { |
25 | arr[i] = createFn(); |
26 | } |
通过运行在Windows XP SP2中的任务管理器可以看到如下结果:
1 | IE6: |
2 | without ` null `: 7.6K -> 20.3K |
3 | with ` null `: 7.6K -> 18K |
4 | IE7: |
5 | without ` null `: 14K -> 29.7K |
6 | with ` null `: 14K -> 27K |
如我们所料,显示断开引用可以释放内存,但是释放的内存不是很多,10000个函数对象才释放大约3M的内存,这对一些小型脚本不算什么,但对于大型程序,或者长时间运行在低内存的设备里的时候,这是非常有必要的。
首先说说什么是内存泄露,在一个进程中,如果某一块内存无法访问,且直到进程结束为止也无法释放,那么就发生了内存泄露。通常这种情况发生在C++之类的手动管理内存的语言编写的程序中,程序员忘记delete或者free会导致内存泄露。本文主要讨论的是浏览器中的内存泄露问题,也就是说,javascript程序导致的内存泄露。
目前为止最权威的关于浏览器内存泄露的文章应该是以下2篇 分别来自微软的()和来自IBM的()
但是这2篇对js语言的认识不够深入,所以讨论的内存泄露问题和解决方案都存在一些偏差。更重要的是,他们太老了,没有介绍IE7的内存泄露新模式。希望本文下面的部分能让读到此文的人更加清晰的认识内存泄露问题。
1.javascript对象的基础知识
(1)创建对象
js中创建对象的方式非常自由,通常有这样几种:直接量、new表达式、内置函数、函数调用。后面是几个例子:
直接量:{"a":10,"b":30}
new表达式:var o=new cls();
内置函数:var e=document.createElement("div");var a=new ActiveXObject("XML2.0.XMLHTTP");
函数调用:function f(){};f();
(2)特殊的对象——作用域对象
值得一提的是函数调用也会创建对象
function f(){
var a=10;
var b=20;
}
复制代码
尽管按照语言标准无法以任何方式访问,但是f函数在每次执行时都会创建一个有属性a和b的对象,这被称为作用域对象。而js将维护一个被称为scopechain的链表,它是一条由当前可访问的所有作用域对象组成的链表。因为js的作用域规则是定义时的作用域,所以每个函数对象被创建时都会以一个属性[[scope]]保存它的外部作用域链。
特别地,在FireFox中,允许用__parent__访问[[scope]]属性所属的函数
关于js的更多,可以去查阅js标准文档ECMA262(HTML版),这里无法完整地介绍js的对象机制。
2.内存泄露的原因
作为一门垃圾回收的语言(注意不是垃圾语言),内存泄露的原因只有一个:引擎的bug(本小节用于休闲、调节气氛)
3.内存泄露的方式
目前发现的可能导致内存泄露的代码有三种:
- 循环引用
- 自动类型装箱转换
- 某些DOM操作
下面具体的来说说内存是如何泄露的
循环引用:这种方式存在于IE6和FF2中(FF3未做测试),当出现了一个含有DOM对象的循环引用时,就会发生内存泄露。
什么是循环引用?首先搞清楚什么是引用,一个对象A的属性被赋值为另一个对象B时,则可以称A引用了B。假如B也引用了A,那么A和B之间构成了循环引用。同样道理 如果能找到A引用B B引用CC又引用A这样一组饮用关系,那么这三个对象构成了循环引用。当一个对象引用自己时,它自己形成了循环引用。注意,在js中变量永远是对象的属性,它可以指向对象,但决不是对象本身。
循环引用很常见,而且通常是无害的,但如果循环引用中包含DOM对象或者ActiveX对象,那么就会发生内存泄露。例子:
var a=document.createElement("div");
var b=new Object();
a.b=b;
b.a=a;
复制代码
很多情况下循环引用不是这样的明显,下面就是著名的闭包(closure)造成内存泄露的例子,每执行一次函数A()都会产生内存泄露。试试看,根据前面讲的scope对象的知识,能不能找出循环引用?
function A()...{
var a=document.createElement("div");
a.οnclick=function()...{
alert("hi");
}
}
A();
复制代码
OK, 让我们来看看。假设A()执行时创建的作用域对象叫做ScopeA 找到以下引用关系
ScopeA引用DOM对象document.createElement("div");
DOM对象document.createElement("div");引用函数function(){alert("hi")}
函数function(){alert("hi")}引用ScopeA
这样就很清楚了,所谓closure泄露,只不过是几个js特殊对象的循环引用而已。
自动类型装箱转换:这种泄露存在于ie6 ie7中。这是极其匪夷所思的一个bug,看下面代码
var s="lalalalala";
alert(s.length);
这段代码怎么了?看看吧,"lalalalala"已经泄露了。关键问题出在s.length上,我们知道js的类型中,string并非对象,但可以对它使用.运算符,为什么呢?因为js的默认类型转换机制,允许js在遇到.运算符时自动将string转换为object型中对应的String对象。而这个转换成的临时对象100%会泄露(汗一下)。
某些DOM操作也可能导致泄露 这些恶心的bug只存在于ie系列中。在ie7中 因为试图fix循环引用bug而让情况变得更糟,以至于我对写这一段种满了恐惧。
从ie6谈起,下面是微软的例子,
<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>Memory Leaking Insert</button>
<button>Clean Insert</button>
<div id="hostElement"></div>
</body>
</html>
复制代码
看看结果吧,LeakMemory造成了内存泄露,而CleanMemory没有,循环引用了么?仔细看看没有。那么是什么问题呢?MS的解释是"插入顺序不对",必须先将父级元素appendChild。这听起来有些模糊,这里给出一个比较恰当的等价描述:永远不要使用DOM节点树之外元素的appendChild方法。
接下来是ie7和ie8 beta1中运行这段程序,看到什么?没看错吧,2个都泄露了!别急,刷新一下页面就好了。为什么呢?ie7改变了DOM元素的回收方式:在离开页面时回收DOM树上的所有元素,所以ie7下的内存管理非常简单:在所有的页面中只要挂在DOM树上的元素,就不会泄露,没挂在DOM树上,肯定泄露。所以,ie7中记住一条原则:在离开页面之前把所有创建的DOM元素挂到DOM树上。
接下来谈谈ie7的这个设计吧,坦白的说,这种做法纯粹是偷懒的垃圾做法。动态垃圾回收不是保证所有内存都在离开页面时收回,而是要保证内存的充分利用,运行时不回收,等到离开时回收有什么用?这只是名义上的避免泄露,其实是完全的泄露。况且还没有回收DOM节点树之外的元素。
4.内存泄露的解决方案
内存泄露怎么办?真的以后不用闭包了么?没法封装控件了?这样做还不如要了js程序员的命,嘿嘿。
事实上,通过一些很简单的小技巧,可以巧妙的绕开这些危险的bug。
to be continued......
coming soon:
- 显式类型转换
- 避免事件导致的循环引用
- 不影响返回值地打破循环引用
- 延迟appendChild
- 代理DOM对象
- 4.内存泄露的解决方案
- 显式类型转换
var s = newString ( "lalalalala" ); //此处将string转换成object alert ( s . length ); DE<复制代码- 避免事件导致的循环引用
function A (){ var a = document . createElement ( "div" ); a . onclick =function(){ alert ( "hi" ); } } DE<复制代码function A (){ var a = document . createElement ( "div" ); var b = document . createElement ( "div" ); a . onclick =function(){ alert ( b . outerHTML ); } return a ; } DE<复制代码function A (){ var a = document . createElement ( "div" ); var b = document . createElement ( "div" ); a . onclick = BuildEvent ( b ); return a ; } function BuildEvent ( b ) { return function(){ alert ( b . outerHTML ); } } DE<复制代码function A (){ try{ var a = document . createElement ( "div" ); var b = document . createElement ( "div" ); a . onclick = function(){ alert ( b . outerHTML ); } return a ; } finally { a = null ; } } DE<复制代码- 延迟appendChild
function appendTo ( Element ) ...{ Element . appendChild ( this ); if(! this . parts )return; for(var i = 0 ; i < this . parts . length ; i ++) parts . appendTo ( this ); } DE<复制代码- 垃圾箱
- 代理对象
- 内存泄露是内存占用很大么? 不是,即使1byte内存也叫做内存泄露。
- 程序中提示,内存不足,是内存泄露么?不是,这一般是无限递归函数调用导致栈内存溢出。
- 内存泄露是哪个区域泄露?堆区,栈区是不会泄露的。
- window对象是DOM对象么?不是,window对象参与的循环引用不会内存泄露。
- 内存泄露后果是什么?大多数时候后果不很严重,但过多DOM操作会导致网页执行变慢。
- 跳转页面后,内存泄露仍然存在么?仍然存在,直到关闭浏览器。
- FireFox也会内存泄露么?FF2仍然有内存泄露
IE内存泄露监测工具 sIEve介绍
在IE下监控页面内存资源和dom节点(sIEve软件使用简介)
先注明一下: 这篇文章只是讲解在IE下如何利用 sIEve(一个drip的加强版) 来发现内存泄露和内存回收问题,
而不是讲解关于 IE的内存管理和内存泄露原理的文章. 关于IE的内存管理和内存泄露的更多知识还麻烦大家自行google了. sIEve 是一个帮助我们查看ie浏览器(他本身基于操作系统安装的ie内核)内存的使用和内存泄露问题的. 它可以:- 列出当前页面内所有dom节点的基本信息(html id style 等)
- 页面内所有dom节点的高级信息 (<SPAN class=hilite2>内存</SPAN>占用,数量,节点的引用)
- 可以查找出页面中的孤立节点
- 可以查找出页面中的循环引用
- 可以查找出页面中产生<SPAN class=hilite2>内存</SPAN>泄露的节点
我下面只是简单介绍一下
运行后, 在上面的address栏内输入要测试的页面地址如 file:///D:/mydev/ie_mem/test_ie.html 出现下图: (图) 下端是内存变化的曲线图右上角是各种功能按钮 右下角是页面内的信息(很重要),该列表自动定时刷新. 5列信息依次是: 内存总体占用量(单位kb) 和上次列表自动刷新时相比,变化的量 当前的dom节点数目 产生内存泄露的节点数目 日志信息(节点发生异常时记录一些信息不常用) 下面开始看一下他是如何使用的 1 ie下不好的移除节点的方式 点击测试页面的"_removeNode div_1"按钮 大家可以看到 div被从页面内移除, 但是看右面的信息列表里 "当前的dom节点数目"并没用变化 点击右上角的功能按钮: show in use. 这时候大家可以在弹出的窗口内看到 id=div_1 的节点是孤立状态而没有被回收 (还有两个孤立节点大家不用管) (图) 2 ie下更好的移除节点的方式 关掉弹出的窗口, 点击测试页面的"removeElement div_2"按钮, div_2被移除而且"当前的dom节点数目"减少 查看show in use. 大家可以看到 div_2及其子节点已经被真的移除了. 上面两个试验演示了如何查看孤立节点, 同时说明了第二种移除节点的方法更有效. 3 循环 关掉弹出的窗口, 点击测试页面的"createCycle div_3"按钮, 然后点击右上角的功能按钮: scan now. (旁边的那个自动检查cycle选项有问题我这里选上后常常会报错) 然后再点show in use. 查看使用中的节点. 大家可以看到 div_3节点形成了 Cycle ,同时看后面的outerHTML大致可以分析出循环的原因. ( style节点也会被当作循环引用, 不知道是ie的问题还是这个软件的问题 ,反正大家没必要在意style) (图) 4 内存泄露 关掉弹出的窗口, 点击测试页面的"createMemLeak div_4"按钮. 之后页面会自动刷新. 大家可以在右边列表里看到 leaks的数量增加了 点击 show leaks 就可以看到内存泄露的点在哪里了 (图) 最后补充一下,在弹出的窗口里还可以做查看节点的更多信息 (双击节点) 下篇为它的官方使用文档