目录

  • 一个存在内存泄露的闭包实例

    • 什么是内存泄露
    • 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,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

  2. 从实践角度:以下函数才算是闭包:

  • 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)

  • 在代码中引用了自由变量

(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 | MDN

Chrome 浏览器垃圾回收机制与内存泄漏分析

4类 JavaScript 内存泄漏及如何避免

深入理解JavaScript系列(12):变量对象(Variable Object)

深入理解JavaScript系列(14):作用域链(Scope Chain)

深入理解JavaScript系列(16):闭包(Closures)

JS中由闭包引发内存泄露的深思的更多相关文章

  1. JS中的闭包(closure)

    JS中的闭包(closure) 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面就是我的学习笔记,对于Javascript初学者应该是很有用 ...

  2. js中的闭包之我理解

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  3. 浅谈JS中的闭包

    浅谈JS中的闭包 在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域. 变量的作用域 变量共有两种,一种为全局变量,一种为局部变量.那么全局变 ...

  4. js中的“闭包”

    js中的“闭包” 姓名:闭包 官方概念:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. ( ⊙o⊙ )!!!这个也太尼玛官方了撒,作为菜鸟的 ...

  5. js中的闭包理解一

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  6. js中的闭包理解

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  7. Android中使用Handler造成内存泄露的分析和解决

    什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...

  8. Android中使用Handler造成内存泄露

    1.什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用 ...

  9. Android 中 Handler 引起的内存泄露

    在Android常用编程中,Handler在进行异步操作并处理返回结果时经常被使用.其实这可能导致内存泄露,代码中哪里可能导致内存泄露,又是如何导致内存泄露的呢?那我们就慢慢分析一下.http://w ...

随机推荐

  1. 单线程IP扫描解析

    扫描代码: private void Button_Click(object sender, RoutedEventArgs e) { a5.Items.Clear(); string str = t ...

  2. matplotlib BlendedAffine2D 和 CompositeAffine2D

    2020-04-11 10:00:01 --Edit by yangrayBlendedAffine2D 继承于Affine2DBase,支持x和y方向使用不同的仿射变换策略.(自译:混合仿射变换)C ...

  3. D3js怎么获得SVG及其子元素在屏幕中的坐标

    var clientRects = svg.select("image").node().getBoundingClientRect(); var coordinates = [ ...

  4. JUC强大的辅助类讲解--->>>CountDownLatchDemo (减少计数)

    原理: CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞.其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞), ...

  5. 五分钟!用python绘制漂亮的系统架构图

    Diagrams 是一个基于Python绘制云系统架构的模块,它能够通过非常简单的描述就能可视化架构,并支持以下6个云产品的图标: AWS.Azure.GCP.K8s.阿里云 和 Oracle 云 基 ...

  6. Daily Scrum 1/18/2016

    Yandong & Zhaoyang: Prepare bug bash slides for Beta release; Dong & Fuchen:Prepare demo for ...

  7. stand up meeting 12/01/2015

    part 组员 今日工作 工作耗时/h 明日计划 工作耗时/h UI 冯晓云   赶工sprint3,各部分要合在一起时出现各种问题,各种修改测试:UI本身的功能继续实现完善    6 UWP对控件的 ...

  8. HBase BucketAllocatorException 异常剖析

    近日,观察到HBase集群出现如下WARN日志: 2020-04-18 16:17:03,081 WARN [regionserver/xxx-BucketCacheWriter-1] bucket. ...

  9. 4. Object

    1. Object.is( );  //用来判断,不同等 == 与===接近.NaN作出的调整 let obj={a:1,b:2}; Object.is(obj,obj);//true Object. ...

  10. RocketMQ存储机制与确认重传机制

    引子 消息队列之前就听说过,但一直没有学习和接触,直到最近的工作流引擎项目用到,需要了解学习一下.本文主要从一个初学者的角度针对RocketMQ的存储机制和确认重传机制做一个浅显的总结. 存储机制 我 ...