关于闭包及变量回收问题

2023-11-03

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

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

问题的提出

假设有如下的代码:

function outer() {
    var largeObject = LargeObject.fromSize('100MB');

    return function() {
        console.log('inner');
    };
}
var inner = outer();

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

如果引入更复杂的情况:

function outer() {
    var largeObject = LargeObject.fromSize('100MB');
    var anotherLargeObject = LargeObject.fromSize('100MB');

    return function() {
        largeObject.work();
        console.log('inner');
    };
}
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

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

function outer() {
    var largeObject = LargeObject.fromSize('100MB');

    return function() {
        debugger;
    };
}
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种情况,进行多用例的测试。

测试过程级结果

基本测试

使用代码
function outer() {
    var largeObject = LargeObject.fromSize('100MB');

    return function() {
        debugger;
    };
}
var inner = outer();
inner();
测试结果
  • JScript.dll – 不回收,内存无下降趋势。
  • Chakra – 回收,内存会恢复到outer函数执行前的状态。
  • Carakan – 不回收,内存无下降趋势。
  • V8 – 回收,访问largeObject抛出ReferenceError。
  • SpiderMonkey – 回收,访问largeObject得到undefined
结论

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

多个变量的情况

使用代码
function outer() {
    var largeObject = LargeObject.fromSize('100MB');
    var anotherLargeObject = LargeObject.fromSize('100MB');

    return function() {
        largeObject;
        debugger;
    };
}
var inner = outer();
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的影响

使用代码
function outer() {
    var largeObject = LargeObject.fromSize('100MB');

    return function() {
        eval('');
        debugger;
    };
}
var inner = outer();
inner();
测试结果
  • JScript.dll – 不回收,内存无下降趋势。
  • Chakra – 不回收,内存无下降趋势。
  • Carakan – 不回收,内存无下降趋势。
  • V8 – 不回收,访问largeObject可得到正确的值。
  • SpiderMonkey – 不回收,访问largeObject可得到正确的值。
结论

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

间接调用eval

使用代码
function outer() {
    var largeObject = LargeObject.fromSize('100MB');

    return function() {
        window.eval('');
        debugger;
    };
}
var inner = outer();
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构造函数的调用。

多个嵌套函数

使用代码
function outer() {
    var largeObject = LargeObject.fromSize('100MB');

    function help() {
        largeObject;
        // eval('');
    }

    return function() {
        debugger;
    };
}
var inner = outer();
inner();
测试结果
  • JScript.dll – 不回收,内存无下降趋势。
  • Chakra – 不回收,内存无下降趋势。
  • Carakan – 不回收,内存无下降趋势。
  • V8 – 不回收,访问largeObject可得到正确的值。
  • SpiderMonkey – 不回收,访问largeObject可得到正确的值。
结论

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

使用with表达式

使用代码
function outer() {
    var largeObject = LargeObject.fromSize('100MB');
    var scope = { o: LargeObject.fromSize('100MB') };

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

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

使用catch表达式

使用代码
function outer() {
    var largeObject = LargeObject.fromSize('100MB');

    try {
        throw { o: LargeObject.fromSize('100MB'); }
    }
    catch (ex) {
        return function() {
            debugger;
        };
    }
}
var inner = outer();
inner();
测试结果
  • JScript.dll – 不回收,内存无下降趋势。
  • Chakra – 回收largeObjectex,内存会恢复到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对象可以回收,则会解除其绑定。

嵌套函数中声明的同名变量

使用代码
function outer() {
    var largeObject = LargeObject.fromSize('100MB');

    return function(largeObject /* 或在函数体内声明 */) {
        // var largeObject;
    };
}
var inner = outer();
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),则:

    1. 如果是V8引擎,则退出全过程。
    2. 如果是SpiderMonkey,则处理该ObjectEnvironment的外层LexicalEnvironment。
  2. 获取当前LexicalEnvironment下的所有类型为Function的对象,对于每一个Function对象,分析其FunctionBody:

    1. 如果FunctionBody中含有直接调用eval,则退出全过程。
    2. 否则得到所有的Identifier。
    3. 对于每一个Identifier,设其为name,根据查找变量引用的规则,从LexicalEnvironment中找出名称为name的绑定binding
    4. binding添加notSwap属性,其值为true
  3. 检查当前LexicalEnvironment中的每一个变量绑定,如果该绑定有notSwap属性且值为true,则:

    1. 如果是V8引擎,删除该绑定。
    2. 如果是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 -> largeObjecthelp -> largeObject并做一个合集来处理,以提高效率。也许这种方式依旧存在内存开销的浪费,但同时CPU资源也是非常贵重的,如何掌握这之间的平衡,便是javascript引擎的选择。

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

关于闭包及变量回收问题 的相关文章

随机推荐

  • Unity使用NavmeshObstacle解决多人寻路终点堵塞问题以及解决NavmeshObstacle打开抖动(瞬移)问题

    不知道为什么 就这个东西 国内各大论坛和网站就是搜不到 最终还是得谷歌 太过基础的就不讲了 问题一 在unity使用navmeshAgent进行多人寻路设置同一个终点后 所有角色都会向对应位置寻路 当前面单位到达后后面单位会一直无法到达导致
  • kiki's game

    http acm hdu edu cn showproblem php pid 2147 Problem Description Recently kiki has nothing to do While she is bored an i
  • Python 全栈系列217 Nginx负载均衡MongoAgent

    说明 虽然不想在完成量化系统的构建前再去分叉搞别的东西 但是在批量计算指标时需要频繁的使用MongoAgent 而这个服务只能做成单线程异步的 所以计算60万次指标需要2 3天时间 考虑到之后可能会有重刷的情况 所以我想还是给MongoAg
  • 被监督写博客-Day7

    今天在ctftime上找了比赛 但是吧 不太行 只能等着明天结束后的wp了 回归刷题日常 题目一 极客大挑战 2019 HardSQL 说真的 真的不喜欢SQL注入的题 打开题目后又是熟悉的界面 看了wp说是报错注入 学习一下两个函数 up
  • jmeter windows 安装指导

    软件安装 Windows安装 软件下载 进入官网 http jmeter apache org 直接下载zip包 下载后直接解压 eg我的解压路径如下 D Program Files apache jmeter 5 5 bin jdk安装
  • 用C++进行设计模式解析和实现

    http c chinaitlab com special sjms Index html 用C 进行设计模式解析和实现
  • 【精】HDFS的HA系列(一)--- 背景、架构

    本文作为HDFS HA系列的第一篇文章 主要简单描述一下HDFS HA的产生背景和整体架构 同时也会对后续系列文章要讲解的内容列出一个大致提纲 一 Hadoop HA背景 单点故障 英语 single point of failure 缩写
  • RabbitMQ快速实战与集群架构详解

    RabbitMQ 1 MQ介绍 1 1 什么是MQ 为什么要用MQ 1 2 MQ的优缺点 1 3 几大MQ产品特点比较 2 Rabbitmq安装 2 1 实验环境 2 2 版本选择 2 3 安装Erlang语言包 2 4 安装RabbitM
  • FFT算法实现

    关于FFT算法的原理这里就不多说了 具体参考有关书籍 DFT与FFT运算量的比较 N点DFT的运算量 复数乘法 复数加法 一个X k N N 1 N个X k N点DFT N N N N 1 N点FFT的运算量 复数乘法 复数加法 N个X k
  • 单片机毕业设计 NodeMCU使用mpu6050惯性传感器

    文章目录 1 简介 MPU6050传感器 引脚连接 相关代码 6 最后 1 简介 Hi 大家好 学长今天向大家介绍 NodeMCU如何使用mpu6050惯性传感器 MPU6050传感器 MPU6050传感器是一个集成了6轴运动跟踪装置的模块
  • LLVM汇编语言指导手册之标识符简介

    标识符 Identifier LLVM的标识符以两种基本的类型存在 全局的和局部的 全局标识符 函数 全局变量 以 字符开始 局部标识符 寄存器名字 类型 以 字符开始 另外 还有三种不同的标识符格式 各有自己的用途 1 指定的 即自己定义
  • Banner————闭组会议总结

    闭组会议总结 一 开场 二 各负责人讲话 三 这学期的感受 四 总结 一 开场 因小组被选为发现杯考场 所以我们在1311小教室举行了这次闭组会议 第一次参加闭组会议 嘿嘿 既激动又有些些伤感 我也不知道激动啥 伤感嘛 就是感觉像要分别一样
  • 一首特别适合冥想的音乐

    这大概是我听过最适合冥想的音乐了 Sudha E Hia Ai 闭上你的双眼 静静地感受当下 感受自己的呼吸 吸气 呼气 如果分心了 回来就好
  • HTML烟花特效,除夕快到了,把它收藏下来,过年和最重要的她一起看烟花。

    文章目录 前言 一 效果图 二 操作步骤 第一步 第二步 第三步 第四步 第五步 第六步 源码 前言 最近随着电视剧 点燃我温暖你 的火热播出 剧中帅气学霸李洵的炫酷爱心代码也迅速火出了圈 爱心素材异常火爆 烟花也异常火爆 毕竟在这绿色生态
  • IMX6学习记录(21)-ubuntu安装QT

    上面是我的微信和QQ群 欢迎新朋友的加入 1 快速安装 命令行安装 sudo apt get install qt5 default qtcreator 用命令行 这个命令就装好了 后面的安装过程不用看了 2 软件包下载 http mirr
  • 【路径规划】基于A*算法和Dijkstra算法的路径规划(Python代码实现)

    欢迎来到本博客 博主优势 博客内容尽量做到思维缜密 逻辑清晰 为了方便读者 座右铭 行百里者 半于九十 本文目录如下 目录 1 概述 2 运行结果 3 参考文献 4 Python代码实现 1 概述 Dijkstra算法是Edsger Wyb
  • jquery获得select选中索引

    select选中索引有好多方式 someId find option selected selectedIndex someId find option selected attr selectedIndex 这两种方式取不到索引值 som
  • Qt应用开发(基础篇)——对话框窗口 QDialog

    一 前言 QDialog类继承于QWidget 是Qt基于对话框窗口 消息窗口QMessageBox 颜色选择窗口QColorDialog 文件选择窗口QFileDialog等 的基类 QDialog窗口是顶级的窗口 一般情况下 用来当做用
  • 确定比赛名次

    http acm hdu edu cn showproblem php pid 1285 Problem Description 有N个比赛队 1 lt N lt 500 编号依次为1 2 3 N进行比赛 比赛结束后 裁判委员会要将所有参赛
  • 关于闭包及变量回收问题

    本文的诞生 源自近期打算做的一个关于javascript中的闭包的专题 由于需要解析闭包对垃圾回收的影响 特此针对不同的javascript引擎 做了相关的测试 为了能从本文中得到需要的知识 看本文前 请明确自己知道闭包的概念 并对垃圾回收