本文的诞生,源自近期打算做的一个关于javascript中的闭包的专题,由于需要解析闭包对垃圾回收的影响,特此针对不同的javascript引擎,做了相关的测试。

为了能从本文中得到需要的知识,看本文前,请明确自己知道闭包的概念,并对垃圾回收的常用算法有一定的了解。

问题的提出
假设有如下的代码:

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3.  
  4. return function() {
  5. console.log('inner');
  6. };
  7. }
  8. var inner = outer();

在这一段代码中,outer函数和inner函数间会形成一个闭包,致使inner函数能够访问到largeObject,但是显然inner并没有访问largeObject,那么在闭包中的largeObject对象是否能被回收呢?

如果引入更复杂的情况:

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3. var anotherLargeObject = LargeObject.fromSize('100MB');
  4.  
  5. return function() {
  6. largeObject.work();
  7. console.log('inner');
  8. };
  9. }
  10. var inner = outer();

首先一个显然的概念是largeObject肯定不能被回收,因为inner确实地需要使用它。但是anotherLargeObject又能不能被回收呢?它将跟随largeObject一起始终存在,还是和largeObject分离,独立地被回收呢?

测试方法
带着这个疑问,对现有的几款现代javascript引擎分别进行了测试,参与测试的有:
~IE8自带的JScript.dll
~IE9自带的Chakra
~Opera 11.60自带的Carakan
~Chrome 16.0.912.63自带的V8(3.6.6.11)
~Firefox 9.0.1自带的SpiderMonkey

测试的基本方案是,使用类似以下的代码:

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3.  
  4. return function() {
  5. debugger;
  6. };
  7. }
  8. var inner = outer();

通过各浏览器的开发者工具(Developer Tools、Firebug、Dragonfly等),在断点处停止javascript的执行,并通过控制台或本地变量查看功能检查largeObject的值,如果其值存在,则认为GC并没有回收该对象。

对于部分浏览器(特别是IE),考虑到对脚本执行有2种模式(执行模式和调试模式,IE通过开发者工具的Script面板中的“Start Debugging”按钮切换),在调试模式下才会命中断点,但是调试模式下可能存在不同的引擎优化方案,因此采用内存比对的方式进行测试。即打开资源浏览器,在var inner = outer();一行后强制执行一次垃圾回收(IE使用window.CollectGarbage();Opera使用window.opera.collect();),查看内存的变化。如果内存始终有100MB的占用,没有明显的下降现象,则认为GC并没有回收该对象。

对于用例的设计,由于从ECMAScript标准中可以得知,所有的变量访问是通过一个LexicalEnvironment对象进行的,因此目标在于在不同的LexicalEnvironment结构下进行测试。从标准中,搜索LexicalEnvironment不难得出能够改变LexicalEnvironment结构的情况有以下几种:

1.进入一个函数。
2.进入一段eval代码。
3.使用with语句。
4.使用catch语句。
因此以下将针对这4种情况,进行多用例的测试。

测试过程级结果
基本测试
使用代码

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3.  
  4. return function() {
  5. debugger;
  6. };
  7. }
  8. var inner = outer();

测试结果
~JScript.dll – 不回收,内存无下降趋势。
~Chakra – 回收,内存会恢复到outer函数执行前的状态。
~Carakan – 不回收,内存无下降趋势。
~V8 – 回收,访问largeObject抛出ReferenceError。
~SpiderMonkey – 回收,访问largeObject得到undefined。

结论
当一个函数outer返回另一个函数inner时,Chakra、V8和SpiderMonkey会对outer中声明,但inner中不使用的变量进行回收,其中V8直接将变量从LexicalEnvironment上解除绑定,而SpiderMonkey仅仅将变量的值设为undefined,并不解除绑定。

多个变量的情况
使用代码

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3. var anotherLargeObject = LargeObject.fromSize('100MB');
  4.  
  5. return function() {
  6. largeObject;
  7. debugger;
  8. };
  9. }
  10. var inner = outer();
  11. inner();

测试结果
~JScript.dll – 不回收,内存无下降趋势。
~Chakra – 回收anotherLargeObject,内存会回到outer调用前并增加100MB左右。
~Carakan – 不回收,内存无下降趋势。
~V8 – 回收,访问largeObject能得到正确的值,访问anotherLargeObject抛出ReferenceError。
~SpiderMonkey – 回收,访问largeObject能得到正确的值,访问anotherLargeObject得到undefined。

结论
当一个LexicalEnvironment上存在多个变量绑定时,Chakra、V8和SpiderMonkey会针对不同的变量判断是否有被使用,该判断方法是扫描返回的函数inner的源码来实现的,随后会将没有被inner使用的变量从LexicalEnvironment中解除绑定(同样的,SpiderMonkey不解除绑定,仅赋值为undefined),而剩下的变量继续保留。

eval的影响
使用代码

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3.  
  4. return function() {
  5. eval('');
  6. debugger;
  7. };
  8. }
  9. var inner = outer();
  10. inner();

测试结果
~JScript.dll – 不回收,内存无下降趋势。
~Chakra – 不回收,内存无下降趋势。
~Carakan – 不回收,内存无下降趋势。
~V8 – 不回收,访问largeObject可得到正确的值。
~SpiderMonkey – 不回收,访问largeObject可得到正确的值。

结论
如果返回的inner函数中有使用eval函数,则不LexicalEnvironment中的任何变量进行解除绑定的操作,保留所有变量的绑定,以避免产生不可预期的结果。

间接调用eval
使用代码

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3.  
  4. return function() {
  5. window.eval('');
  6. debugger;
  7. };
  8. }
  9. var inner = outer();
  10. inner();

测试结果
~JScript.dll – 不回收,内存无下降趋势。
~Chakra – 回收,内存会恢复到outer函数执行前的状态。
~Carakan – 不回收,内存无下降趋势。
~V8 – 回收,访问largeObject抛出ReferenceError。
~SpiderMonkey – 回收,访问largeObject得到undefined。

结论
由于ECMAScript规定间接调用eval时,代码将在全局作用域下执行,是无法访问到largeObject变量的。因此对于间接调用eval的情况,各javascript引擎将按标准的方式进行处理,无视该间接调用eval的存在。
同样的,对于new Function(‘return largeObject;’)这种情形,由于标准规定new Function创建的函数的[[Scope]]是全局的LexicalEnvironment,因此也无法访问到largeObject,所有引擎都参照间接调用eval的方式,选择无视Function构造函数的调用。

多个嵌套函数
使用代码

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3.  
  4. function help() {
  5. largeObject;
  6. // eval('');
  7. }
  8.  
  9. return function() {
  10. debugger;
  11. };
  12. }
  13. var inner = outer();
  14. inner();

测试结果
~JScript.dll – 不回收,内存无下降趋势。
~Chakra – 不回收,内存无下降趋势。
~Carakan – 不回收,内存无下降趋势。
~V8 – 不回收,访问largeObject可得到正确的值。
~SpiderMonkey – 不回收,访问largeObject可得到正确的值。

结论
不仅仅是被返回的inner函数,如果在outer函数中定义的嵌套的help函数中使用了largeObject变量(或直接调用eval),也同样会造成largeObject变量无法回收。因此javascript引擎扫描的不仅仅是inner函数的源码,同样扫描了其他所有嵌套函数的源码,以判断是否可以解除某个特定变量的绑定。

使用with表达式
使用代码

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3. var scope = { o: LargeObject.fromSize('100MB') };
  4.  
  5. with (scope) {
  6. return function() {
  7. debugger;
  8. };
  9. }
  10. }
  11. var inner = outer();
  12. inner();

测试结果
~JScript.dll – 不回收,内存无下降趋势。
~Chakra – 回收largeObject,但不回收scope.o,内存恢复至outer函数被调用前并增加100MB左右(无法得知scope是否被回收)。
~Carakan – 不回收,内存无下降趋势。
~V8 – 不回收,访问largeObject和scope以及o均可得到正确的值。
~SpiderMonkey – 回收largeObject和scope,访问该2个变量均得到undefined,不回收o,可得到正确的值。

结论
当有with表达式时,V8将会放弃所有变量的回收,保留LexicalEnvironment中所有变量的绑定。而SpiderMonkey则会保留由with表达式生成的新的LexicalEnvironment中的所有变量的绑定,而对于outer函数生成的LexicalEnvironment,按标准的方式进行处理,尽可能解除其中的变量绑定。

使用catch表达式
使用代码

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3.  
  4. try {
  5. throw { o: LargeObject.fromSize('100MB'); }
  6. }
  7. catch (ex) {
  8. return function() {
  9. debugger;
  10. };
  11. }
  12. }
  13. var inner = outer();
  14. inner();

测试结果
~JScript.dll – 不回收,内存无下降趋势。
~Chakra – 回收largeObject和ex,内存会恢复到outer函数被调用前的状态。
~Carakan – 不回收,内存无下降趋势。
~V8 – 仅回收largeObject,访问largeObject抛出ReferenceError,但仍可访问到ex。
~SpiderMonkey – 仅回收largeObject,访问largeObject得到undefined,但仍可访问到ex。

结论
catch表达式虽然会增加一个LexicalEnvironment,但对闭包内变量的绑定解除算法几乎没有影响,这源于catch生成的LexicalEnvironment仅仅是追加了被catch的Error对象一个绑定,是可控的(相对的with则不可控),因此对变量回收的影响也可以控制和优化。但对于新生成并添加了Error对象的LexicalEnvironment,V8和SpiderMonkey均不会进一步优化回收,而Chakra则会对该LexicalEnvironment进行处理,如果其中的Error对象可以回收,则会解除其绑定。

嵌套函数中声明的同名变量
使用代码

  1. function outer() {
  2. var largeObject = LargeObject.fromSize('100MB');
  3.  
  4. return function(largeObject /* 或在函数体内声明 */) {
  5. // var largeObject;
  6. };
  7. }
  8. var inner = outer();
  9. inner();

测试结果
~JScript.dll – 不回收,内存无下降趋势。
~Chakra – 回收,内存会恢复到outer函数被调用前的状态。
~Carakan – 不回收,内存无下降趋势。
~V8 – 回收,内存会恢复到outer函数被调用前的状态。
~SpiderMonkey – 回收,内存会恢复到outer函数被调用前的状态。

结论
嵌套函数中有与外层函数同名的变量或参数时,不会影响到外层函数中该变量的回收优化。即javascript引擎会排除FormalParameterList和所有VariableDeclaration表达式中的Identifier,再扫描所有Identifier来分析变量的可回收性。

总体结论
首先一个较为明确的结论是,以下内容会影响到闭包内变量的回收:
~嵌套的函数中是否有使用该变量。
~嵌套的函数中是否有直接调用eval。
~是否使用了with表达式。

Chakra、V8和SpiderMonkey将受以上因素的影响,表现出不尽相同又较为相似的回收策略,而JScript.dll和Carakan则完全没有这方面的优化,会完整保留整个LexicalEnvironment中的所有变量绑定,造成一定的内存消耗。

由于对闭包内变量有回收优化策略的Chakra、V8和SpiderMonkey引擎的行为较为相似,因此可以总结如下,当返回一个函数fn时:
1.如果fn的[[Scope]]是ObjectEnvironment(with表达式生成ObjectEnvironment,函数和catch表达式生成DeclarativeEnvironment),则:
A.如果是V8引擎,则退出全过程。
B.如果是SpiderMonkey,则处理该ObjectEnvironment的外层LexicalEnvironment。

2.获取当前LexicalEnvironment下的所有类型为Function的对象,对于每一个Function对象,分析其FunctionBody:
A.如果FunctionBody中含有直接调用eval,则退出全过程。
B.否则得到所有的Identifier。
C.对于每一个Identifier,设其为name,根据查找变量引用的规则,从LexicalEnvironment中找出名称为name的绑定binding。
D.对binding添加notSwap属性,其值为true。

3.检查当前LexicalEnvironment中的每一个变量绑定,如果该绑定有notSwap属性且值为true,则:
A.如果是V8引擎,删除该绑定。
B.如果是SpiderMonkey,将该绑定的值设为undefined,将删除notSwap属性。
对于Chakra引擎,暂无法得知是按V8的模式还是按SpiderMonkey的模式进行。

从以上测试及结论来看,V8确实是一个优秀的javascript引擎,在这一方面的优化相当到位。而SpiderMonkey则采取一种更为友好的方式,不直接删除变量的绑定,而是将值赋为undefined,也许是SpiderMonkey团队考虑到有一些极端特殊的情况,依旧有可能导致使用到该变量,因此保证至少不会抛出ReferenceError打断代码的执行。而IE9的Chakra相比IE8的JScript.dll进步非常大,细节上的处理也很优秀。Opera的Carakan在这一方面则相对落后,完全没有对闭包内的变量回收进行优化,选择了最为稳妥但略显浪费的方式。

此外,所有带有优化策略的浏览器,都在内在开销和速度之间选择了一个平衡点,这也正是为什么“多个嵌套函数”这一测试用例中,虽然inner没有再使用largeObject对象,甚至在inner中的断点处,连help函数对象也已经解除绑定,却没有解除largeObject的绑定。基于这种现象,可以推测各引擎均只选择检查一层的关联性,即不去处理inner -> help -> largeObject这样深度的引用关系,只找inner -> largeObject和help -> largeObject并做一个合集来处理,以提高效率。也许这种方式依旧存在内存开销的浪费,但同时CPU资源也是非常贵重的,如何掌握这之间的平衡,便是javascript引擎的选择。

此外,根据部分开发者的测试,Chakra甚至有资格被称为现有最快速的javascript引擎,微软也一直在努力,而开发者更不应该一味地谩骂和嘲笑IE。
我们可以嘲笑IE6的落后,可以看不到低版本的IE曾经为互联网的发展做过的贡献,可以在这些历史产品已经没落的今天无情地给予打击,却最最不应该将整个IE系列一视同仁,挂上“垃圾”的名号。客观地去看待,去评价,正是一个技术人员应该具备的最基本的准则和素养。

js闭包测试的更多相关文章

  1. 大部分人都会做错的经典JS闭包面试题

    由工作中演变而来的面试题 这是一个我工作当中的遇到的一个问题,似乎很有趣,就当做了一道题去面试,发现几乎没人能全部答对并说出原因,遂拿出来聊一聊吧. 先看题目代码: function fun(n,o) ...

  2. 【闭包】JS闭包深入理解

    先看题目代码: 1 2 3 4 5 6 7 8 9 10 11 12 function fun(n,o) {  console.log(o)  return {   fun:function(m){ ...

  3. js闭包的作用域以及闭包案列的介绍:

    转载▼ 标签: it   js闭包的作用域以及闭包案列的介绍:   首先我们根据前面的介绍来分析js闭包有什么作用,他会给我们编程带来什么好处? 闭包是为了更方便我们在处理js函数的时候会遇到以下的几 ...

  4. Js闭包常见三种用法

        Js闭包特性源于内部函数可以将外部函数的活动对象保存在自己的作用域链上,所以使内部函数的可以将外部函数的活动对象占为己有,可以在外部函数销毁时依然存有外部函数内的活动对象内容,这样做的好处是可 ...

  5. js闭包之初步理解( JavaScript closure)

    闭包一直是js中一个比较难于理解的东西,而平时用途又非常多,因此不得不对闭包进行必要的理解,现在来说说我对js闭包的理解. 要理解闭包,肯定是要先了解js的一个重要特性, 回想一下,那就是函数作用域, ...

  6. (原创)JS闭包看代码理解

    <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="C ...

  7. js闭包理解

    js闭包的作用是使函数外可以访问函数内部的变量,是通过 在函数内部 定义 访问函数内变量 的函数实现的,内部的一个函数产生一个闭包 function a() { var i=0; return fun ...

  8. js闭包理解实例小结

    Js闭包 闭包前要了解的知识  1. 函数作用域 (1).Js语言特殊之处在于函数内部可以直接读取全局变量 <script type="text/javascript"> ...

  9. Js闭包的用途

    本来想总结一点JavaScript中的闭包的一些用法,在查资料的时候发现了一篇很好的文章,就转过来收藏了,下面附上传送门: js闭包的用途 ---------sunlylorn 我们来看看闭包的用途. ...

随机推荐

  1. Android应用更新升级实现

    介绍 在产品的开发中,android升级提示,下载更新是必备的功能,否则等用户被动去官方网,或者第三方商店提示,就为时已晚了. 原理 在用户每次打开应用的时候,都与服务器进行一次交互,获取版本信息,对 ...

  2. cmd.exe-应用程序错误 应用程序无法正常启动(0xc0000142)

    之前还好好的,突然就遇到这个问题,运行CMD报错(如上图),后面无论怎么重启都是这样. 导致所有与CMD相关的程序任务都出错,例如Ctrl+Alt+Delete 只好开始各种百度谷歌 找到如下几种解决 ...

  3. Java编程思想 (1~10)

    [注:此博客旨在从<Java编程思想>这本书的目录结构上来检验自己的Java基础知识,只为笔记之用] 第一章 对象导论 1.万物皆对象2.程序就是对象的集合3.每个对象都是由其它对象所构成 ...

  4. ab做压力测试

    ab是apache 自带的一个压力测试的小工具,可用于接口简单的压力测试. 以下是AB的简要介绍 格式:ab [options] [http://]hostname[:port]/path 参数说明: ...

  5. android 自定义组件-带图片的textView

    1. 定义属性 <?xml version="1.0" encoding="utf-8"?> <resources> <decla ...

  6. 【自动化测试】Selenium 下载文件

    用curl确定要下载的文件是什么类型的:另一种方法是使用requests 模块来查找内容类型 文件类型 http://tool.oschina.net/commons 1.先设置下载的目录,下载文件的 ...

  7. POJ 1942 Paths on a Grid

    // n*m 的格子 从左下角走到右上角的种数// 相当于从 n+m 的步数中选 m 步往上走// C(n+m,m) #include <iostream> #include <st ...

  8. Android 线程与消息 机制 15问15答

    1.handler,looper,messagequeue三者之间的关系以及各自的角色? 答:MessageQueue就是存储消息的载体,Looper就是无限循环查找这个载体里是否还有消息.Handl ...

  9. 页面异步加载javascript文件

    昨天听一同事说的异步加载js文件,可以提高页面加载速度.具体方法如下:(function() {  var ga = document.createElement('script'); ga.type ...

  10. EhCache 分布式缓存/缓存集群

    开发环境: System:Windows JavaEE Server:tomcat5.0.2.8.tomcat6 JavaSDK: jdk6+ IDE:eclipse.MyEclipse 6.6 开发 ...