js 内存泄漏


什么是内存泄漏?

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。

char * buffer;
buffer = (char*) malloc(42);// Do something with bufferfree(buffer);

上面是 C 语言代码,malloc方法用来申请内存,使用完毕之后,必须自己用free方法释放内存。

这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"(garbage collector)。

虽然前端有垃圾回收机制,但当某块无用的内存,却无法被垃圾回收机制认为是垃圾时,也就发生内存泄漏了。

哪些情况会引起内存泄漏

1. 意外的全局变量

全局变量的生命周期最长,直到页面关闭前,它都存活着,所以全局变量上的内存一直都不会被回收。

当全局变量使用不当,没有及时回收(手动赋值 null),或者拼写错误等将某个变量挂载到全局变量时,也就发生内存泄漏了。

2. 遗忘的定时器
setTimeout 和 setInterval 是由浏览器专门线程来维护它的生命周期,所以当在某个页面使用了定时器,当该页面销毁时,没有手动去释放清理这些定时器的话,那么这些定时器还是存活着的。

也就是说,定时器的生命周期并不挂靠在页面上,所以当在当前页面的 js 里通过定时器注册了某个回调函数,而该回调函数内又持有当前页面某个变量或某些 DOM 元素时,就会导致即使页面销毁了,由于定时器持有该页面部分引用而造成页面无法正常被回收,从而导致内存泄漏了。

如果此时再次打开同个页面,内存中其实是有双份页面数据的,如果多次关闭、打开,那么内存泄漏会越来越严重。而且这种场景很容易出现,因为使用定时器的人很容易遗忘清除。

3. 使用不当的闭包
函数本身会持有它定义时所在的词法环境的引用,但通常情况下,使用完函数后,该函数所申请的内存都会被回收了。

但当函数内再返回一个函数时,由于返回的函数持有外部函数的词法环境,而返回的函数又被其他生命周期东西所持有,导致外部函数虽然执行完了,但内存却无法被回收。

4. 遗漏的 DOM 元素
DOM 元素的生命周期正常是取决于是否挂载在 DOM 树上,当从 DOM 树上移除时,也就可以被销毁回收了
但如果某个 DOM 元素,在 js 中也持有它的引用时,那么它的生命周期就由 js 和是否在 DOM 树上两者决定了,记得移除时,两个地方都需要去清理才能正常回收它。

5. 网络回调
某些场景中,在某个页面发起网络请求,并注册一个回调,且回调函数内持有该页面某些内容,那么,当该页面销毁时,应该注销网络的回调,否则,因为网络持有页面部分内容,也会导致页面部分内容无法被回收。


如何监控内存泄漏

内存泄漏是可以分成两类的,一种是比较严重的,泄漏的就一直回收不回来了,另一种严重程度稍微轻点,就是没有及时清理导致的内存泄漏,一段时间后还是可以被清理掉。

不管哪一种,利用开发者工具抓到的内存图,应该都会看到一段时间内,内存占用不断的直线式下降,这是因为不断发生 GC,也就是垃圾回收导致的。

内存不足会造成不断 GC,而 GC 时是会阻塞主线程的,所以会影响到页面性能,造成卡顿,所以内存泄漏问题还是需要关注的。

场景一:在某个函数内申请一块内存,然后该函数在短时间内不断被调用

// 点击按钮,就执行一次函数,申请一块内存
startBtn.addEventListener("click", function() {var a = new Array(100000).fill(1);var b = new Array(20000).fill(1);
});

一个页面能够使用的内存是有限的,当内存不足时,就会触发垃圾回收机制去回收没用的内存。

而在函数内部使用的变量都是局部变量,函数执行完毕,这块内存就没用可以被回收了。

所以当我们短时间内不断调用该函数时,可以发现,函数执行时,发现内存不足,垃圾回收机制工作,回收上一个函数申请的内存,因为上个函数已经执行结束了,内存无用可被回收了。

所以图中呈现内存使用量的图表就是一条横线过去,中间出现多处竖线,其实就是表示内存清空,再申请,清空再申请,每个竖线的位置就是垃圾回收机制工作以及函数执行又申请的时机。

场景二:在某个函数内申请一块内存,然后该函数在短时间内不断被调用,但每次申请的内存,有一部分被外部持有。

// 点击按钮,就执行一次函数,申请一块内存
var arr = [];
startBtn.addEventListener("click", function() {var a = new Array(100000).fill(1);var b = new Array(20000).fill(1);arr.push(b);
});

看一下跟第一张图片有什么区别?

不再是一条横线了吧,而且横线中的每个竖线的底部也不是同一水平了吧。

其实这就是内存泄漏了。

我们在函数内申请了两个数组内存,但其中有个数组却被外部持有,那么,即使每次函数执行完,这部分被外部持有的数组内存也依旧回收不了,所以每次只能回收一部分内存。

这样一来,当函数调用次数增多时,没法回收的内存就越多,内存泄漏的也就越多,导致内存使用量一直在增长
另外,也可以使用 performance monitor 工具,在开发者工具里找到更多的按钮,在里面打开此功能面板,这是一个可以实时监控 cpu,内存等使用情况的工具,会比上面只能抓取一段时间内工具更直观一点:

梯状上升的就是发生内存泄漏了,每次函数调用,总有一部分数据被外部持有导致无法回收,而后面平滑状的则是每次使用完都可以正常被回收。

这张图需要注意下,第一个红框末尾有个直线式下滑,这是因为,我修改了代码,把外部持有函数内申请的数组那行代码去掉,然后刷新页面,手动点击 GC 才触发的效果,否则,无论你怎么点 GC,有部分内存一直无法回收,是达不到这样的效果图的。

以上,是监控是否发生内存泄漏的一些工具,但下一步才是关键,既然发现内存泄漏,那该如何定位呢?如何知道,是哪部分数据没被回收导致的泄漏呢?

如何分析内存泄漏,找出有问题的代码

分析内存泄漏的原因,还是需要借助开发者工具的 Memory 功能,这个功能可以抓取内存快照,也可以抓取一段时间内,内存分配的情况,还可以抓取一段时间内触发内存分配的各函数情况。

利用这些工具,我们可以分析出,某个时刻是由于哪个函数操作导致了内存分配,分析出大量重复且没有被回收的对象是什么。

这样一来,有嫌疑的函数也知道了,有嫌疑的对象也知道了,再去代码中分析下,这个函数里的这个对象到底是不是就是内存泄漏的元凶,搞定。

先举个简单例子,再举个实际内存泄漏的例子:

场景一:在某个函数内申请一块内存,然后该函数在短时间内不断被调用,但每次申请的内存,有一部分被外部持有

// 每次点击按钮,就有一部分内存无法回收,因为被外部 arr 持有了
var arr = [];
startBtn.addEventListener("click", function() {var a = new Array(100000).fill(1);var b = new Array(20000).fill(1);arr.push(b);
});


可以抓取两份快照,两份快照中间进行内存泄漏操作,最后再比对两份快照的区别,查看增加的对象是什么,回收的对象又是哪些,如上图。

也可以单独查看某个时刻快照,从内存占用比例来查看占据大量内存的是什么对象,如下图:

还可以从垃圾回收机制角度出发,查看从 GC root 根节点出发,可达的对象里,哪些对象占用大量内存:

从上面这些方式入手,都可以查看到当前占用大量内存的对象是什么,一般来说,这个就是嫌疑犯了。

当然,也并不一定,当有嫌疑对象时,可以利用多次内存快照间比对,中间手动强制 GC 下,看下该回收的对象有没有被回收,这是一种思路。

  • 抓取一段时间内,内存分配情况。

这个方式,可以有选择性的查看各个内存分配时刻是由哪个函数发起,且内存存储的是什么对象。

当然,内存分配是正常行为,这里查看到的还需要借助其他数据来判断某个对象是否是嫌疑对象,比如内存占用比例,或结合内存快照等等。

  • 抓取一段时间内函数的内存使用情况

这个能看到的内容很少,比较简单,目的也很明确,就是一段时间内,都有哪些操作在申请内存,且用了多少。

总之,这些工具并没有办法直接给你答复,告诉你 xxx 就是内存泄漏的元凶,如果浏览器层面就能确定了,那它干嘛不回收它,干嘛还会造成内存泄漏

所以,这些工具,只能给你各种内存使用信息,你需要自己借助这些信息,根据自己代码的逻辑,去分析,哪些嫌疑对象才是内存泄漏的元凶。

实例分析

例子1:

var t = null;
var replaceThing = function() {var o = tvar unused = function() {if (o) {console.log("hi")}        }t = {longStr: new Array(100000).fill('*'),someMethod: function() {console.log(1)}}
}
setInterval(replaceThing, 1000)

先说说这代码用途,声明了一个全局变量 t 和 replaceThing 函数,函数目的在于会为全局变量赋值一个新对象,然后内部有个变量存储全局变量 t 被替换前的值,最后定时器周期性执行 replaceThing 函数

  • 发现问题

我们先利用工具看看,是不是会发生内存泄漏:

三种内存监控图表都显示,这发生内存泄漏了:反复执行同个函数,内存却梯状式增长,手动点击 GC 内存也没有下降,说明函数每次执行都有部分内存泄漏了。

这种手动强制垃圾回收都无法将内存将下去的情况是很严重的,长期执行下去,会耗尽可用内存,导致页面卡顿甚至崩掉。

  • 分析问题

既然已经确定有内存泄漏了,那么接下去就该找出内存泄漏的原因了。

首先通过 sampling profile,我们把嫌疑定位到 replaceThing 这个函数上

接着,我们抓取两份内存快照,比对一下,看看能否得到什么信息:

比对两份快照可以发现,这过程中,数组对象一直在增加,而且这个数组对象来自 replaceThing 函数内部创建的对象的 longStr 属性。

其实这张图信息很多了,尤其是下方那个嵌套图,嵌套关系是反着来,你倒着看的话,就可以发现,从全局对象 Window 是如何一步步访问到该数组对象的,垃圾回收机制正是因为有这样一条可达的访问路径,才无法回收。

其实这里就可以分析了,为了多使用些工具,我们换个图来分析吧。

我们直接从第二份内存快照入手,看看:

为什么每一次 replaceThing 函数调用后,内部创建的对象都无法被回收呢?

因为 replaceThing 的第一次创建,这个对象被全局变量 t 持有,所以回收不了。

后面的每一次调用,这个对象都被上一个 replaceThing 函数内部的 o 局部变量持有而回收不了。

而这个函数内的局部变量 o 在 replaceThing 首次调用时被创建的对象的 someMethod 方法持有,该方法挂载的对象被全局变量 t 持有,所以也回收不了。

这样层层持有,每一次函数的调用,都会持有函数上次调用时内部创建的局部变量,导致函数即使执行结束,这些局部变量也无法回收。

JavaScript 内存泄漏相关推荐

  1. 什么是javascript内存泄漏?以及解决方法

    什么是javascript内存泄漏?以及解决方法 一.什么是javascript内存泄漏? 二.常见的内存泄漏 1.意外的全局变量(通常是变量未被定义或者胡乱引用了全局变量) 2.计时器 3.闭包 4 ...

  2. 介绍两个非常好用的Javascript内存泄漏检测工具

    内存泄漏对开发者来说一般很难检测因为它们是由一些大量代码中的意外的错误引起的,但它在系统内存不足前并不影响程序的功能.这就是为什么会有人在很长时间的测试期中收集应用程序性能指标来测试性能. 最简单的检 ...

  3. JavaScript内存泄漏知多少?

    垃圾回收解放了我们,它让我们可将精力集中在应用程序逻辑(而不是内存管理)上.但是,垃圾收集并不神奇.了解它的工作原理,以及如何使它保留本应在很久以前释放的内存,就可以实现更快更可靠的应用程序.在本文中 ...

  4. javascript内存泄漏调试工具mac_node.js 内存泄漏的秘密

    一直以来,跟踪 Node.js 的内存泄漏是一个反复出现的话题,人们始终希望对其复杂性和原因了解更多. 并非所有的内存泄漏都显而易见.但是,一旦我们确定了其模式,就必须在内存使用率,内存中保存的对象和 ...

  5. javascript内存泄漏调试工具mac_Vuex3.1.1更新:支持jsDelivr,修复内存泄漏

    JavaScript 已成为庞大.多样化并快速发展的编程语言.每当 JS 的框架或库发布更新,社区中与之相关的项目也会随之作出改进--Vue.js 及其附属项目就是典型例子. Vuex 是简单直观的状 ...

  6. Meta开源JavaScript内存泄漏监测工具MemLab

    一.MemLab简介 上周,Facebook母公司Meta 宣布了开源 MemLab,一个基于 Chromium 的浏览器的 JavaScript 应用程序内存泄漏监测工具.同时,Facebook 技 ...

  7. 排查Javascript内存泄漏

    引言 也许你已经知道,Chrome DevTools里的Performance面板和Memory面板可以用来定位内存问题.但当你真正上手使用它们的时候,往往会觉得不知所措 -- 因为里面有着各种各样的 ...

  8. JavaScript内存泄漏定位

    本文转自JS内存泄漏排查方法--Chrome Profiles 概述 Google Chrome浏览器提供了非常强大的JS调试工具,Heap Profiling便是其中一个.Heap Profilin ...

  9. Javascript内存泄漏与优化

    1. 什么是内存泄漏 内存泄漏是指在Javascript中,已经分配内存地址的对象由于长时间未释放或者是无法释放造成了长期占用内存,使内存浪费,最终导致运行的应用响应速度变慢以及最终崩溃的情况. 2. ...

  10. JavaScript内存泄漏

    内存泄漏简介 内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果. 内存泄漏缺陷具有隐蔽性. ...

最新文章

  1. C#Dictionary源码
  2. [转]kaldi中的特征提取
  3. 随机生成50个字段的elasticsearch的测试程序输入
  4. 限制Apache日志access.log、error.log文件大小
  5. CentOS 6.5/6.6 安装mysql 5.7 最完整版教程
  6. python数据库操作批量sql执行_利用Python如何批量修改数据库执行Sql文件
  7. 出于安全考虑,谷歌禁用三款 Linux web 浏览器登录其服务
  8. 转移印花技术基础知识及应用方法
  9. 正则化的logistic回归
  10. File Juicer for Mac(文件内容提取工具)
  11. VirtualBox 打开虚拟机后,上面的那栏菜单栏不见了的解决办法
  12. linux系统进入图文界面,Linux启动界面切换:图形界面-字符界面
  13. 《Cocos Creator游戏实战》打砖块主要功能与逻辑实现
  14. 特长生模拟——买装备
  15. HDU5442(字符串的最大表示法或者后缀数组)
  16. echart图片库_基于html5 Canvas图表库 : ECharts
  17. 如何换ionic里面的图标
  18. Python搭建一个系统信息实时监控数据可视化大屏
  19. js 中日期转换成时间戳
  20. JQuery data方法的使用-遁地龙卷风

热门文章

  1. 小学信息技术课程目录
  2. 如何下载安装最新版本的 Android Studio
  3. 寄存器、RAM、ROM、Flash
  4. 思考gRPC :为什么是HTTP/2
  5. 网站建设时如何提高网站打开速度?
  6. 面试题:数据库索引失效的条件
  7. 六年间全球十大勒索攻击事件盘点:没有底线全是算计
  8. matlab 图片保存失真,如何将matlab画出的图片保存为要求精度
  9. 查看git分支从哪个源分支创建的
  10. 中国奇人周兴和——记四川星河建材有限公司董事长周兴和(上)