Closure, Memory Leak and Memory Diagnose

what is Closure

从概念上讲, Closure是一种语言机制,它的出现是为了满足函数式编程(functional programming lauguage)的某种设计需求, 即,为在函数式语言中作为first-class的函数对象提供与之绑定的数据环境

维基百科上说Closure是"a function or reference to a function together with a referencing enviroment - a table of storing non-local variables(or called free variable)"Jim Ley这篇文章介绍Closure是:is an expression (typically a function) that can have free variables together with an environment that binds those variables (that "closes" the expression)这里Jim Ley Closure文章的中译版。

实现支持Closure机制的语言,最初是一些函数式语言,如Lisp, 传统命令型语言(imperative programming lanaguage)并不支持Closure特性,如C, Pascal。 但近年来,现代语言向支持多范型编程的方向发展,像C#, Ruby, Javascript都是支持functional, imperative和object-oriented programming, 它们都实现了Closure机制。原本抵触Closure的Java社区也计划在java 8 中支持Closure。

举个例子来说明在Javascript中Closure这个概念是如何被表达的以及对应的各个部分。

e.g.1
var fn = (function(arg){    // ]------> enviroment that contain free variable to a funcitoin.
	var free_variable = 100 //自由变量.
	return function(local){  //the closure function(first-class epression)
		alert(free_variable + local + arg);
	}

})(100);  //]--->

fn(10);
fn(20);

Javascript编译器实现的Closure机制包含着一些关键数据结构包括:Scope chain, function instance, Call object, Excution Context等;scope chain即是Javascript中一个被调用的函数或者未来将被调用的function intance的可见变量作用域。scope chain上的除链表顶端的第一个对象外,其他对象所对应的都是Closure定义中的"与function绑定的环境中自由变量"。有些java或C++程序员觉得说白了这不就是描述着函数变量的可见性问题么?是这样的。scope chain的机制就是表达的这个。但是结合"注1"的参考,读者需要理解的是Js引擎中Scope Chain以及其他数据结构以及机制只是为了实现Closure这个设计需求的手段,Scope Chain只是如何表达Closure中的local variable和free variable的一种实现。一言以蔽之, 类是有行为的数据,Closure则是有数据的行为。

有很多JS书籍和资料都讲述了在javascript中闭关实现机制的相关内容,包括Scope Chain, function instance, Call object, Execution Context. 推荐阅读犀牛书, 周爱民老师的精髓中的章节,前面引用过的Jim Ley的文章等。09年我也写过一篇博客,"有关闭包"在这里但从msn空间被新浪的搬家程序把页面搞乱了,而且,当时有个别地方我混淆了函数的动态链和静态链,时间已久我不再修改了,仅供参考。

注1:first-class大致是不可分割,不可重复的之意。其意和元类型或基础类型相近,在函数式编程中,function作为first-class参与运算。

Memory Leak

浏览器中发生Memory Leak是指,页面某个动作所申请的内存无法被GC所回收。而页面内持续的触发该效果的动作导致页面占用内存持续上升,直到该页面被关闭为止。

经典情况是,当出现包含着DOM元素的对象间的循环引用. 由于旧版本的DOM组件模型中的基于ref-counting garbage collection和JS引擎的mark-and-sweep garbage collectioin不能很好的协调工作,导致该循环引用无法被GC。Douglas Crockford这篇文章是较早的介绍ML的资料,我测试了一下他写的3个testcase(queue test1,2,3),在IE9以及IE9的IE7模式下已经不再引发内存泄露。另外一篇IE memory leak-revisited则贴出了作者测试用例和测试结论。但实际上,Memory Leak不仅仅发生于包含DOM元素的循环引用,更一般的,如果在web应用中,对对象的引用被持续保持(由js程序或js引擎),那么该内存则不被释放。这是真正的原因。可以参考Google Open Source blog的leak Finder:a new tool for Javascript的讲述。这篇了解 JavaScript 应用程序中的内存泄漏举了有js引擎持有定时器的回调函数所导致的对象引用被持久持有的例子。

Closure的结构中包含着循环引用(inner function的scope chain指向它的静态定义域-outer function,而outer function为了可以访问inner function,有指向inner function的内部引用)。事件注册的场景既是一个典型的Closure,同时包含着DOM元素循环引用,可参见例2。但是之所以常常提到IE中的Memory Leak,则是因为兼容性处理方面的需求。实际上,Firefox和IE都可能会触发Memory Leak Pattern. Memory Leak patterns in JS这篇文章明确指出微软的COM组件模型和Mozilla的XPCOM组件模型都采用ref-count garbage collection, 在不当的情形下,都会引发ML。MDN社区提供的有关资料非常丰富。详见memory诊断调优一节。

要留意的是,这个包含DOM的循环引用模式在表现形式上其实是很多样的, 这篇Understanding and Solving Internet Explorer Leak Patterns介绍了4种典型的ML情形(Closure, Cross page leak, pesudo-leak, etc.)。

function on(elem, type, handler){
	elem.addEventListner && 
	(elem.addEventLister(type, handler, false),1) ||  //for FF or those comform to DOM level2 standard
							  //there is no need to switch the context of hander;
	(elem.attachEvent && 
	elem.attachEvent('on'+type, wrapper));            //for IE, we switch its this value to elem using a closure.
							  //which falls into ML pattern.
	function wrapper(e){
		hanlder.call(elem, e);
	}
}

参考例2的注释。为了避免在业务逻辑代码进行现场判断处理兼容性问题, 并且在handler中统一的让this指向被注册的DOM节点,处理兼容性问题常常在事件注册这一层次完成。因此如果代码不特别留意这一点,在IE中则很容易引发ML。解决方案有很多,让wrapper function的scope chain指向on function的外围,从而截断DOM元素与handler函数之间的引用关系,是常见方法。MDN上的A re-introduction to JavaScript介绍了有趣的一种嵌套闭包的方式来截断。我也写了篇博客文章在这里, 强调了2处不易注意到的tip:

Memory诊断、调优和最佳实践

随着浏览器的发展,某些经典的Memory Leak模式已经不再引发ML,或者至少进行了某些改进。例如: IEBlog组宣称IE7已经对管理循环引用做出了提升,在页面离开时,会把对象的全部引用释放到js engine端,从而这样JS GC可以回收那些资源。拥有window sp2的IE6也有做了如前面提到的改进。(参见Tools for Detecting Memory Leaks929874)

但同时,对作为一个Jser对内存的理解和要求,也已经不能停留在"如何写出不堕入到Memory Leak Pattern陷阱的侵入式脚本、自有框架、业务逻辑组件的代码"的层次。web应用越来越富客户端化,时间越来越长。需要有手段量化的检测程序生命周期中内存的使用情况。实际上,对于Jser来说,深进的了解与内存有关的性能调优更高也更实际的需求。以下罗列了一些工具:

  • IEblog组提供了2个工具针对IE trident引擎的工具:DripsIEve
  • MDN社区资料很丰富。Debugging memory leaks介绍了Mozilla团队针对商用工具Purify的一些自有开发的增强工具。(BloatView, RefCount Tracing, etc.), Performance:Leak Tools是上述工具的基本概念和使用的介绍。performance tools,这篇则列举了20多个内存工具。特别留意的是,FF有基于XPCOM的本地扩展机制,因此web应用的内存检测其实也涉及针对这类可以操作本地文件系统的机制的扩展。另外,FF的addon插件库中提供了memory monitor, memory profiler, Cycle collector analyzer三个与memory调试有关的插件。但前两个不支持较新版本的FF。(当然稍微改动一下这类组件的源码这类组件也可以被安装)。
  • Google的Chrome提供的Developer Tool当中的Heap Profiler已经具有很好的内存调试的能力。Heap profiler提供对当前内存的快照和多次快照之间的比较视图等功能,可以记录到两次快照间每个对象被增加和删除的状态(deleted和delta字段)。而timeline可以记录内存整体持续的变化的状态。这篇这篇介绍了Heap profiler的使用方法。这篇则提供了不错的分析实例。Google 团队还提供了一个叫leak finder for Javascript的工具。另外,chromium的这篇文章可供深入分析Chrome内存问题的索引。
  • 除此之外,有一个state.js可以记录web应用js代码的性能。state.js会提供一个info box,显示代码所使用内存。尽管有些人认为这种形式的侦测并不可能很准确,但这个确实是一个很有趣的应用;

随着web应用的富客户端化,web内容组件化等趋势,Jser应当掌握各个平台上的内存分析工具。在平时开发大型应用时,可以分解应用的生命期,查看是否有不断侵占内存的情况出现。一些简单的最佳实践的思路包括:

  • 要同时考虑和对象的创建与析构。复杂的结构的对象的销毁应当调用子对象的析构方法。虽然Javascript并没有要求严格的析构动作,但应当考虑诸如在合适的时间注销事件处理器等问题。
  • 应当特别留意定时器的使用和析构;
其他有关最佳实践方面的话题,我列举了几篇: IE + JavaScript Performance Recommendations:part1, part2, part3, IE8 Performance, re-intro to JS, 但可参考的内容还有很多。

Test case and Test result

我大致测试了一下IE6 - IE10, chrome27, Firefox21, Firefox3.6 中,经典Memory Leak Pattern(包含DOM元素的循环引用)造成的影响。考虑到ML pattern表现形式的多样,test case的设计还可以丰富。由于时间关系仅采用了最经典的2种情形。


test case : test1.html

test case desc: 我修改了一下Douglas的queue_test2测试用例, 增加了时间戳和用户触发事件以方便记录结构。Douglas的这个测试用例,是基于DOM 1的注册事件方式,该测试用例插入10000个dom节点,为插入节点注册事件,然后删除
 Memory Leaktest envtest result
IE6yeswindowXP,sp1,IE6+quirks doctypeinsert 10000 dom. time elapsed 156.5 s,
t1 pf usage: 107M;
t2, pf usage: 215M
although DOM nodes were removed from dom tree, but they are not truely released and js engin also dont GC the handlers function instance. the ref-count GC for DOM and mark-and-sweep GC for JS can't coroporate well for each other.
see pic1,2;
IE7nowindows 7, IE10 forced,to IE7 modet1 pf: 1502M, 1527M,
t2 pf: 1509M, 1527M.
there are small wave in amount of memory usage.pf usage raise in small account but it can be drop, when operation finished, it drop the value almost equal to that of at start.
IE8,9,10nowin7+IE10 force to IE8,9,10 mode; 
chrome27no t1 snapshot : we app memory usage: 967k,
t2 snapshot: we app memory usage: 1.4M;
Heap proflier timeline: timeline的memory图指出在chrome在不断删除DOM元素时,GC程序在积极的动作进行内存回收。参见pic4.
heap proflier的snapshot的compare view指出,start动作后内存的增长来最大部分来自于array和system,都是js内置对象部分,分别是113k和115k,closure和DOM元素均无大规模增长。
Firefox21, 3.6no  

test case2 : test2.html

测试用例2使用DOM2注册事件api方式以及IE的attachEvent。其他同test case1。
 Memory Leaktest envtest result
IE6yeswindowXP,sp1,IE6+quirks doctypeinsert 10000 dom. time elapsed 159.1 s,
t1 pf usage: 206M;
t2, pf usage: 325M
using attachEvent method to register handler in IE6 lead to more memory leak. inserting 10000 dom element costs 119M memory can't be GCed
see pic3.
see pic1,2;
IE7nowindows 7, IE10 forced,to IE7 modet1 pf: 1451M,
t2 pf: 1451M
time elapsed: 100.6 s
inserting 10000 dom element even with cyclic Closure involed DOM would NOT cause IE7 leak memory.
IE8,9,10nowin7+IE10 force to IE8,9,10 mode; 
chrome27no timeline指出GC事件发生2次,积极回收内存呢,但结束时,大量内存被持有时间较长;
t1,t2 snapshot比较,内存增量小于testcase1.
t1: 1.1M, t2: 1.5M
Firefox21, 3.6no  

picture 1: IE6 test case1 开始任务之前:



picture 2: IE6 test case1 任务完成



picture 3: IE7 任务执行完成:



pictur 4: chrome test case1 任务执行完成



pictur 5: chrome test case2 任务执行完成


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值