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 ...
随机推荐
- spark 大杂烩
累加器 val dataRdd = sc.makeRDD(List(1, 2, 3, 4), 2) var sum = 0 //累加器可以收集driver和各个excuter中累加的结果 //如果此处 ...
- JVM崩溃的原因及解决!
JVM崩溃的原因及解决! 前些天,搞JNI的时候,报了个JVM崩溃的错.错误信息如下: # # An unexpected error has been detected by HotSpot Vir ...
- Java面向对象的总结
面向对象的程序设计 1.面向对象 核心:以类的方式组织代码,以对象的方式封装数据 比喻:也就是说类是没有数据的,给了数据之后的类就是对象 封装 继承 多态 2.方法 a.一个方法只有一个返回值,只有一 ...
- AJ学IOS 之微博项目实战(8)用AFNetworking和SDWebImage简单加载微博数据
AJ分享,必须精品 一:效果 没有图文混排,也没有复杂的UI,仅仅是简单的显示出微博数据,主要介绍AFNetworking和SDWebImage的简单用法 二:加载数据AFNetworking AFN ...
- phoenix 索引实践
准备工作 创建测试表 CREATE TABLE my_table ( rowkey VARCHAR NOT NULL PRIMARY KEY, v1 VARCHAR, v2 VARCHAR, v3 V ...
- JAVA—HashMap
一些关于hashmap的学习笔记 1.HashMap底层实现原理 在JDK1.7中HashMap是以数组加链表的形式组成的,在JDK1.8之后新增了红黑树的组成结构,当链表大于8并且容量大于64时,链 ...
- L3 多层感知机
**本小节用到的数据下载 1.涉及语句 import d2lzh1981 as d2l 数据1 : d2lzh1981 链接:https://pan.baidu.com/s/1LyaZ84Q4M75G ...
- Daily Scrum 12/24/2015
Process: Zhaoyang: Some UI change and compile the Caffe in the IOS. Yandong: Do some code integratio ...
- Serval and Parenthesis Sequence CodeForces - 1153C
题目大意:一个字符串只含有? ( ),?可以变成 ) 或者 ( ,将字符串中所有的?变成) 或者 ( 使得字符串合法. 合法就是让括号配对,并且不可以提前结束比如:()()这样是不合法的. 题解:既然 ...
- JS Math&Date的方法 (上)
数学对象&时间对象 本篇文章主要介绍Math 和 Date 的常用方法! 一 :Math & Date Math 数学对象 - 处理数学计算和数学类 ...