JS中由闭包引发内存泄露的深思
目录
- 一个存在内存泄露的闭包实例
- 什么是内存泄露
- JS的垃圾回收机制
- 什么是闭包
- 什么原因导致了内存泄露
- 参考
1.一个存在内存泄露的闭包实例
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
上面代码片段做了一件事情:每隔1秒后调用 replaceThing 函数,全局变量 theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包。
初看之下,感觉应该不存在什么内存泄露问题。replaceThing 函数在每次调用完之后,应该就会释放或销毁 originalThing 和 unused 变量,毕竟这两个变量只在函数内部声明使用了,不能够在 replaceThing 函数外面被使用。而留在内存中的就只剩每次新分配给全局变量 theThing 的新对象。
但实际上面的直观感受是错误,因为没有真正理解到闭包的实现原理。为了弄清楚上面的代码为什么存在内存泄露,我们首先需要弄清楚几个概念与原理:什么是内存泄露?JS的垃圾回收机制?什么是闭包?
(1)什么是内存泄露
应用程序不再用到的内存,由于某些原因,没有及时释放,就叫做内存泄漏。
(2)JS的垃圾回收机制
不同的编程语言管理内存的方式各不相同。一些高级编程语言的解释器或运行时嵌入了“垃圾回收器”,通过算法可自动的进行内存的分配与释放管理(比如 JavaScript、Java、C# 等)。另一些则寄希望于开发者自己手动地进行内存的分配与释放管理(比如 C/C++ 等)。
而JavaScript 是通过垃圾回收器来进行内存管理,其实现是基于标记-清除算法。而这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。其假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。在标记过程,垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。标记完成后就进行清除过程。(可达内存被标记,其余的被当作垃圾回收。)
(3)什么是闭包。
开发人员经常错误将闭包简化理解成从父上下文中返回内部函数,或则简单归纳为能够读取其他函数内部变量的函数。
实际上,根据 ECMAScript,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量
(4)什么原因导致了内存泄露
我们这里主要以实践角度来理解我们所讨论的闭包。
这里需要弄明白一个问题:为什么创建闭包函数的函数的上下文已经被销毁了(常规理解就是函数调用栈释放,函数内的临时变量被回收等),闭包函数依旧可以读取创建它的函数的内部变量?
从结果倒推,唯一能解释这一点的就是:虽然创建闭包函数的函数的上下文已经被销毁了,但被闭包函数所引用的变量没有被回收。那具体是如何实现的呢?
为了深入理解这个问题,这里就需要简单的谈一下函数中的作用域链:
当前函数的作用域链[[Scope]] = VO / AO + 父级函数的作用域链[[Scope]]
补充说明:VO 和 AO 分别表示变量对象和活动对象,而变量对象可以理解为保存了当前上下文数据(变量、函数声明、函数参数)的一个对象,而活动对象是特殊的变量对象,简单理解就是函数的变量对象我们一般称之为活动对象,而在全局上下文里,全局对象自身就是变量对象。点击查看详细解释
在JS内部实现中,每个函数都会有一个 [[Scope]] 属性,表示当前函数的可以访问的作用域链。其实质上就是一个对象数组,包含了函数能够访问到的所有标识符(变量、函数等),用以查找函数所使用的到的标识符。而数组中从左到右的对象依次对应了由内到外的其他函数(或全局)的活动(变量)对象。另外,在 ECMAScript 中,同一个父上下文中创建的闭包是共用一个 [[Scope]] 属性的。换句话说,同一个函数内部的所有闭包共用这个函数的 [[Scope]] 属性。
对于闭包函数来说,为了实现其所引用的变量不会被回收,会保留它的作用域链(即 [[Scope]] 属性),不会被垃圾回收器回收。
那么上面的示例中,闭包函数 unused 与 someMethod 的作用域链如下图所示(函数和对象名加了数字后缀,用以区分replaceThing 函数多次调用而产生的同名函数与对象)
(1)replaceThing 函数第一次调用:

如上图,在 replaceThing 函数第一次调用完,通过全局变量 theThing,可以访问到闭包函数 someMehtod1,因此其作用域链也会被保留,即 replaceThing1.[[Scope]] 将被保留,所以闭包函数 unused1就算没有被使用,也不会被回收。(全局变量直到程序运行结束前都不会被回收)
(2)replaceThing 函数第二次调用:

如上图,在 replaceThing 函数第二次调用完,通过全局变量 theThing,可以访问到闭包函数 someMehtod2,因此其作用域链也会被保留,即 replaceThing2.[[Scope]] 将被保留,所以闭包函数 unused2 与对象 originalThing2 也将被保留,不会被回收。由于 originalThing2 可以访问到闭包函数 someMehtod1,因此之前第一次被保留的作用域链仍将继续被保留。
当 replaceThing 函数继续重复调用时,相当于上图中虚线框中的内容不断重复,而且相互之间类似形成一个链表,通过 全局变量 theThing 可以顺着链表到查找到第一次调用产生的对象 [Object1],这也就导致了垃圾回收器无法回收每次产生的新对象(里面包含一个大数组和一个闭包),造成严重的内存泄漏。
2.参考
深入理解JavaScript系列(12):变量对象(Variable Object)
深入理解JavaScript系列(14):作用域链(Scope Chain)
深入理解JavaScript系列(16):闭包(Closures)
JS中由闭包引发内存泄露的深思的更多相关文章
- JS中的闭包(closure)
JS中的闭包(closure) 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面就是我的学习笔记,对于Javascript初学者应该是很有用 ...
- js中的闭包之我理解
闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...
- 浅谈JS中的闭包
浅谈JS中的闭包 在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域. 变量的作用域 变量共有两种,一种为全局变量,一种为局部变量.那么全局变 ...
- js中的“闭包”
js中的“闭包” 姓名:闭包 官方概念:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. ( ⊙o⊙ )!!!这个也太尼玛官方了撒,作为菜鸟的 ...
- js中的闭包理解一
闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...
- js中的闭包理解
闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...
- Android中使用Handler造成内存泄露的分析和解决
什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...
- Android中使用Handler造成内存泄露
1.什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用 ...
- Android 中 Handler 引起的内存泄露
在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用.其实这可能导致内存泄露,代码中哪里可能导致内存泄露,又是如何导致内存泄露的呢?那我们就慢慢分析一下.http://w ...
随机推荐
- Python zipfile模块学习
转载自https://www.j4ml.com/t/15270 import zipfile import os from zipfile import ZipFile class ZipManage ...
- Git应用详解第六讲:Git协作与Git pull常见问题
前言 前情提要:Git应用详解第五讲:远程仓库Github与Git图形化界面 git除了可以很好地管理个人项目外,最大的一个用处就是实现团队协作开发.况且,linus大神开发git的初衷就是为了维护L ...
- vue项目中使用bpmn-流程图预览篇
前情提要 上文已经实现了节点操作的前进.后退.导入.导出等操作,今日来实现“流程图预览”,以及视图的放大缩小 前提:项目安装过bpmn,安装可见上篇文章 实现要点 bpmn提供了两个神器:Modele ...
- python3(二十) module
# 在Python中,一个.py文件就称之为一个模块(Module) # 1.最大的好处是大大提高了代码的可维护性. # 2.可以被其他地方引用 # 3.python内置的模块和来自第三方的模块 # ...
- 06-jmeter参数化(函数对话框使用)
概念: 1.变量命名的规则:字母.下划线开头,可包含数字,严格区分大小写 2.配置元件:用户定义的变量-------值是不变化的 用户命名的参数--------可以动态获取的并传参的 jmeter函数 ...
- CORS漏洞的学习与分析
同源策略 同源策略(Same origin policy)是一种约定,一种非常重要的安全措施,也是最基本的安全功能,它禁止了来自不同源的脚本对当前页面的读取或修改,从而限制了跨域访问甚至修改资源,防止 ...
- AJ学IOS 之tableView的下拉放大图片的方法
AJ分享,必须精品 一:效果 tableview下拉的时候上部分图片放大会 二:代码 直接上代码,自己研究吧 #import "NYViewController.h" //图片的高 ...
- day7作业
# day7作业 # 1. 使用while循环输出1 2 3 4 5 6 8 9 10 count = 1 while count < 11: if count == 7: count += 1 ...
- webstorm tslint配置
webstorm设置 settings >> TypeScript >> TSLint, 勾选 Enable ,选取 tslint包路径...npm\node_modules\ ...
- js上传文件前判断获取文件大小并且加以判断
描述:要求浏览器单个上传文件大小不超过10M. 解决方案: var fileSize = $("#fileId")[0].files[0].size/(1024*1024);if( ...