手把手教你如何排查Javascript内存泄漏

目录
  • 引言
  • 如何判断我的应用发生了内存泄漏
  • Performance和Memory都可以用来定位内存问题,先用谁呢
  • 通过Memory面板定位内存泄漏的流程通常是怎么样的呢
  • 为什么我的内存快照记录下来之后看不懂,还出现了很多奇怪的变量
  • 快照里有一些“Detached DOM tree”,是什么意思
  • Shallow size 和 Retained size,它们有什么不同
  • Memory里的Summary视图, Comparison视图, Dominators视图和Containment视图分别有什么不同呢
    • Summary view:
    • Comparison view:
    • Containment view:
    • Statistics view:
  • Constructor下的(array), Array, (closure), (compiled code)都对应的哪些内容?
  • 发现有一个叫feedback_cell的字段经常出现,它是什么?是它导致了内存泄漏吗?
  • 常见的内存泄漏场景有哪些?
  • 结语

引言

也许你已经知道,Chrome DevTools里的Performance面板和Memory面板可以用来定位内存问题。但当你真正上手使用它们的时候,往往会觉得不知所措 —— 因为里面有着各种各样的选项和功能,让人眼花缭乱。下面我会通过一些常见的FAQ来带大家一起学习怎么用工具定位javascript里的内存问题。

如何判断我的应用发生了内存泄漏

为了证明螃蟹的听觉在腿上,一个专家捉了只螃蟹并冲它大吼,螃蟹很快就跑了。然后捉回来再冲它吼,螃蟹又跑了。最后专家把螃蟹的腿都切了,又对着螃蟹大吼,螃蟹果然一动不动……

定位内存问题的过程其实也类似,如果你自己都不知道自己的页面在使用过程中哪些步骤会导致内存增长,那很可能就会错把一个正常的内存增长当作内存泄漏来排查,最后查了半天白忙活。 其实一个单页应用在使用过程中,内存发生增长是很合理的。例如在开发过程中,为了优化使用体验,我们可能会对部分数据进行缓存,这部分缓存的数据其实也会导致内存占用的升高,但它是符合预期的。因此,排查内存泄漏的第一步,就是要先梳理一遍自己的代码,看一下哪部分内存的升高是合理的,哪部分内存的升高是不合理的。

Performance和Memory都可以用来定位内存问题,先用谁呢

答案是先用Performance。 当我们怀疑页面发生了内存泄漏的时候,可以先用Performance录制一段时间内页面的性能变化。你只需要切换到Performance面板,点击Record,然后在页面上正常操作一段时间,最后停止录制即可。

不断升高的内存下限

如果录制结束后,看到内存的下限在不断升高的话,你就要注意了 —— 这里有可能发生了内存泄漏。

除了内存增长曲线,Nodes(Dom节点数曲线)、Document曲线以及Listener曲线也同样值得关注,有时候它们对内存问题的定位也很有帮助。

当你怀疑发生了内存泄漏的时候,你就可以用Memory面板来进一步定位泄漏的源头了。

通过Memory面板定位内存泄漏的流程通常是怎么样的呢

通常,我们可以从Memory的主界面开始,点击左上角的圆点就可以记录下当前的堆内存快照(heap snapshot)了。

Memory面板

这里推荐一个Gmail团队也在用的 “three snapshot”技巧:

  • 打开DevTools, 切换至Memory面板
  • 先记录一个堆内存快照
  • 在你的页面上执行可能发生泄漏的操作
  • 再记录一个堆内存快照
  • 重复执行多几遍步骤3
  • 最后记录一个堆内存快照
  • 选择最后一个堆内存快照,找到顶栏的“All objects”, 切换至”Objects allocated between snapshots 1 and 2”(也可以对2,3执行同样的操作)

过滤出两份快照之间新分配的对象

8. 切换后,你就能看到两个快照之间新生成的对象。你可以选择其中一项点开,看看它的retaining tree里面保留了哪些对象没有释放。

Tips:在记录第一个堆快照之前你可以先做一些“预热”操作,避免一些懒加载和缓存策略影响到了对内存的分析。

为什么我的内存快照记录下来之后看不懂,还出现了很多奇怪的变量

这也是我排查内存泄漏时遇到的第一个问题,为什么教程里的内存快照简洁易懂,我的内存快照却像一本天书?

教程里的内存快照

我的内存快照

为什么有这么大的差异呢?除去教程里demo代码比较简单之外,提前准备好一个合理的debug环境也是很重要的。这里我列举了4点个人觉得对debug内存问题很有帮助的措施:

1. 尽量使用没有混淆的代码:

打包后的代码往往经过了混淆和压缩,在生产环境上这是必要的,但在debug时却会成为我们的绊脚石,不便于阅读。

2. 排查问题时使用production模式编译出来的代码:

Dev模式下往往会开启一些方便开发的特性,例如热更新等。但它们可能会占用一部分的内存,影响到内存问题的排查,所以建议还是使用production模式编译出来的代码进行问题排查。

3. 屏蔽所有浏览器插件:

屏蔽浏览器插件最快的方式就是打开无痕窗口。浏览器插件给我们带来很多便利,但插件注入的额外逻辑有时也会影响内存问题的排查。例如vue-devtools会记录下每一个vuex mutaions,导致内存无法释放。

4. 在现场打内存快照,便于跳转到源代码所在行:

尽管devTools记录下来的内存快照文件可以单独加载展示,但还是建议在记录下内存快照的时候“趁热”分析,因为这时还能从retaining tree上跳转到代码所在行,有时候对定位问题也很有帮助。

跳转到源码所在行

快照里有一些“Detached DOM tree”,是什么意思

一个DOM节点只有在没有被页面的DOM树或者Javascript引用时,才会被垃圾回收。当一个节点处于“detached”状态,表示它已经不在DOM树上了,但Javascript仍旧对它有引用,所以暂时没有被回收。通常,Detached DOM tree往往会造成内存泄漏,我们可以重点分析这部分的数据。

Shallow size 和 Retained size,它们有什么不同

Shallow size: 这是对象自身占用内存的大小。通常只有数组和字符串的shallow size比较大。

Retain size: 这是将对象本身连同其无法从 GC 根到达的相关对象一起删除后释放的内存大小。 因此,如果Shallow Size = Retained Size,说明基本没怎么泄漏。而如果Retained Size > Shallow Size,就需要多加注意了。

Memory里的Summary视图, Comparison视图, Dominators视图和Containment视图分别有什么不同呢

Summary view:

顾名思义,Summary view就是当前内存快照的一个概览。我们先介绍一下这个视图下的每一列是什么意思: - Constructor: 对象的构造器。 - Distance:与root的距离。距离越大,处理和加载这个对象的时间就越长。 - Object Count:指定构造器创建的对象的数量。 - Shallow Size:对象自身占用内存的大小。 - Retained Size:释放掉该对象后,能释放掉的内存。

在这个视图下你可以看到当前页面内存的具体构成,但如果想定位内存问题,下面的Comparison view会更加有用。

Comparison view:

Comparison视图可以让你对比两份内存快照之间的差异。默认是跟上一份快照做对比,当然你也可以选择任意两份内存做对比。这个视图下每一列的数据有点不同: - Constructor: 对象的构造器。 - # New: 该对象构造器下有多少新对象被创建 - # Deleted: 该对象构造器下有多少新对象被销毁 - # Delta: # New - # Delete的差值 - Alloc.Size:两份快照之间新分配的内存 - Freed Size: 两份快照之间释放掉的内存 - Size Delta:Alloc Size - Freed Size 的差值

这个视图绝对是排查内存泄漏的利器。当你能定位到是哪些操作可能造成内存泄漏后,比较操作前后的内存快照,很容易就能发现发生内存泄漏的对象。

Containment view:

Containment view提供了一个自下而上的视图,它允许你浏览和探索堆内存的内容。我们可以用它来分析一些全部变量的引用情况(如window)。

Statistics view:

Statistics视图会用饼图的形式展示各个类型对象的内存占比

Constructor下的(array), Array, (closure), (compiled code)都对应的哪些内容?

  • (closure): 函数闭包持有的内存引用。
  • (array, string, number, regex): 包含着一系列对象,这些对象的属性上有对应类型变量的引用。
  • (compiled code): Javascript引擎(如V8)为了加快运行速度,会对代码进行一次编译。(compiled code)顾名思义就是指与编译后的代码相关联的内存。
  • Detached HTMLDivElement等:代码里对指定类型Dom节点的引用。

发现有一个叫feedback_cell的字段经常出现,它是什么?是它导致了内存泄漏吗?

经常出现的feedback_cell

放心,它不会造成内存泄漏。它是v8对频繁运行的热代码做出的优化,会被v8自己回收。详见这篇文章:Feedback vectors in heap snapshots – Rohit Pagariya

常见的内存泄漏场景有哪些?

这里列举了一些常见的内存泄漏场景,遇到内存泄漏问题时可以先自查一遍常见场景,个人感觉能解决日常开发中遇到的90%内存泄漏

  • console导致的内存泄漏 因为打印后的对象需要支持在控制台上查看,所以传递给console.log方法的对象是不能被垃圾回收的。我们需要避免在生产环境用console打印对象。
  • 框架配合第三方库使用时,没有及时执行销毁 这点可以参考vue cookbook里的例子
  • 被遗忘的定时器 例如在组件初始化的时候设置了setInterval,那么在组件销毁之前记得调用clearInterval方法取消定时器。
  • 没有正确移除事件监听器(各种EventBus, dom事件监听等) 这应该是最容易犯的一个错误,无论新手老手都有可能栽在这里。
    特征:performance里,监听器数量会持续上升

持续上升的监听器数量

啰嗦一句:尽管大部分同学都会有主动移除监听器的观念,但如果姿势不对,可能依旧会造成内存泄漏。下面是一个真实案例:

// 版本一
mounted() {
    window.addEventListener('resize', debounce(this.handleWidthChange, 100))
},
beforeDestroy() {
    window.removeEventListener('resize', debounce(this.handleWidthChange, 100))
}

乍一看好像写的还不错,有及时移除监听器,对resize这种频繁触发的事件也加了debounce处理。但其实这段代码就导致了内存泄漏:每次调用debounce(this.handleWidthChange, 100)时, 其实都会返回一个新的函数,导致addEventListener和 removeEventListener方法传入的回调函数已经不是同一个回调函数,监听器没有被正确移除,内存泄漏。

下面来看修改后的代码:

// 版本二
data() {
    return {
        debounceWidthChange: null
    }
},
mounted() {
    this.debounceWidthChange = debounce(this.handleWidthChange, 100)
    window.addEventListener('resize', this.debounceWidthChange)
},
beforeDestroyed() {
    window.removeEventListener('resize', this.debounceWidthChange)
}

修改后,监听和移除监听的已经是同一个回调函数了,看起来似乎已经没问题。然而,这段代码还是有内存泄漏的问题。没看出问题的小伙伴可以对比一下正确答案:

// 版本三
data() {
    return {
        debounceWidthChange: null
    }
},
mounted() {
    this.debounceWidthChange = debounce(this.handleWidthChange, 100)
    window.addEventListener('resize', this.debounceWidthChange)
},
beforeDestroy() {
    window.removeEventListener('resize', this.debounceWidthChange)
}

是的,答案非常狗血:Vue只有destroyedbeforeDestroy这两个生命周期,没有 beforeDestroyed,所以上面的beforeDestroyed函数永远不会执行,导致了内存泄漏…

结语

简单总结一下排查内存泄漏的常见流程:

1. 用performance面板记录操作一段时间内的内存变化,找出可能发生内存泄漏的操作。

2. 用“three snapshot”技巧,记录下发生泄漏前后的内存快照

3. 用comparison视图对泄漏前后的内存快照进行比较,找出泄漏的对象。

4. 重点关注 Vue Component, Detached HTMLDivElement等Constructor 。

以上就是手把手教你如何排查Javascript内存泄漏的详细内容,更多关于Javascript内存泄漏排查的资料请关注我们其它相关文章!

(0)

相关推荐

  • JavaScript内存泄漏的处理方式

    下面就是小编整理的关于JS遇到内存泄漏问题时应该采取的处理方式. 随着现在的编程语言功能越来越成熟.复杂,内存管理也容易被大家忽略.本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题. 概述 像C语言这样的编程语言,具有简单的内存管理功能函数,例如malloc( )和free( ).开发人员可以使用这些功能函数来显式地分配和释放系统的内存. 当创建对象和字符串等时,JavaScript就会分配内存,并在不再使用时自动释

  • 一文搞懂如何避免JavaScript内存泄漏

    目录 一.什么是内存泄漏 二.常见的内存泄漏 1.意外的全局变量 2. 计时器 3. 闭包 4. 事件监听器 5.缓存 6.分离的DOM元素 三.识别内存泄漏 1.使用性能分析器可视化内存消耗 2. 识别分离的 DOM 节点 大家好,我是CUGGZ.SPA(单页应用程序)的兴起,促使我们更加关注与内存相关的 JavaScript 编码实践.如果应用使用的内存越来越多,就会严重影响性能,甚至导致浏览器的崩溃.下面就来看看JavaScript中常见的内存泄漏以及如何避免内存泄漏. 一.什么是内存泄漏

  • JS常见内存泄漏及解决方案解析

    内存泄漏? 官方解释:内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果. 通俗点就是指由于疏忽或者错误造成程序未能释放已经不再使用的内存,不再用到的内存却没有及时释放,从而造成内存上的浪费. 避免内存泄漏? 在局部作用域中,等函数执行完毕,变量就没有存在的必要了,垃圾回收机制很亏地做出判断并且回收,但是对于全局变量,很难判断什么时候不用这些变量,无法正常回收:所以,尽量少使用全局变量.在

  • JS造成内存泄漏的几种情况实例分析

    本文实例讲述了JS造成内存泄漏的几种情况.分享给大家供大家参考,具体如下: 介绍: js中的内存垃圾回收机制:垃圾回收器会定期扫描内存,当某个内存中的值被引用为零时就会将其回收.当前变量已经使用完毕但依然被引用,导致垃圾回收器无法回收这就造成了内存泄漏.传统页面每次跳转都会释放内存,所以并不是特别明显. Vue单页面应用中:Web App 与 传统Web的区别,因为Web App是单页面应用页面通过路由跳转不会刷新页面,导致内存泄漏不断堆积,导致页面卡顿. 泄漏点: 1.DOM/BOM 对象泄漏

  • 一篇文章弄懂javascript内存泄漏

    1.什么是内存泄漏 在了解什么是内存泄漏之前, 我们应该要对内存是什么有个概念, 随机存取存储器(英语:Random Access Memory,缩写:RAM)是与 CPU 直接交换数据的内部存储器.它可以随时读写, 而且速度很快,通常作为操作系统或其他正在运行中的程序的临时资料存储介质. 什么是内存泄漏? : 程序不再需要使用的内存, 但是又没有及时释放, 就叫做内存泄漏! 然后在理解泄漏之前, 我们的了解下内存的管理, 在一些底层语言中, 如C语言, 内存是需要开发者自己分配和释放的, 通过

  • 手把手教你如何排查Javascript内存泄漏

    目录 引言 如何判断我的应用发生了内存泄漏 Performance和Memory都可以用来定位内存问题,先用谁呢 通过Memory面板定位内存泄漏的流程通常是怎么样的呢 为什么我的内存快照记录下来之后看不懂,还出现了很多奇怪的变量 快照里有一些“Detached DOM tree”,是什么意思 Shallow size 和 Retained size,它们有什么不同 Memory里的Summary视图, Comparison视图, Dominators视图和Containment视图分别有什么不

  • 一篇文章教你如何排查.NET内存泄漏

    目录 前言 检查托管内存使用 生成dump文件 分析 core dump 总结 前言 内存泄漏通常表示:一个应用程序的某些对象在完成它的的生命周期后,由于它被其他对象意外引用,导致后续gc无法对它进行回收,长此以往就会导致程序性能的下降以及潜在的 OutOfMemoryException. 这篇我们通过一个内存泄漏工具对 .NET Core 程序进行内存泄漏分析,如果程序是跑在windows上,那直接可以使用 Visual Studio 进行诊断. 检查托管内存使用 在开始分析内存泄漏之前,你一

  • 详谈JavaScript内存泄漏

    1.什么是闭包.以及闭包所涉及的作用域链这里就不说了. 2.JavaScript垃圾回收机制 JavaScript不需要手动地释放内存,它使用一种自动垃圾回收机制(garbage collection).当一个对象无用的时候,即程序中无变量引用这个对象时,就会从内存中释放掉这个变量. 复制代码 代码如下: var s = [ 1, 2 ,3];     var s = null;     //这样原始的数组[1 ,2 ,3]就会被释放掉了. 3.循环引用 三个对象 A .B .C AàBàC :

  • Meta开源JavaScript内存泄漏监测工具MemLab安装使用

    目录 一.MemLab简介 二.工作原理 三.基本使用 3.1 安装与使用 3.2 堆分析与研究 3.3 Memlab API 3.4 内存断言 一.MemLab简介 上周,Facebook母公司Meta 宣布了开源 MemLab,一个基于 Chromium 的浏览器的 JavaScript 应用程序内存泄漏监测工具.同时,Facebook 技术团队指出:“应用程序的性能和功能正确性问题通常会被用户立即留意到.然而内存泄漏却不一样,它不容易被立即察觉,但它每次都会吃掉一大块内存,使得整个网络会话

  • 插件:检测javascript的内存泄漏

    转自:http://www.ajaxjs.com/yuicn/bbs/ShowPost.asp?ThreadID=6 2006-10-18 @ 07:59:29 · 作者 volcano Javascript的内存泄漏,不是太可怕.它只会悄悄的,慢慢的把你的浏览器拖的巨慢无比,让你愤怒的拍案而起,大骂微软出品的破烂浏览器危害社会.这一切有可能并不是浏览器的错,可能只是因为网页上有些javascript的内存泄漏罢了. 在科技日益发达今天,我们有必要武装自己,以及自己的浏览器,这样万一浏览器倒下了

  • GoLang内存泄漏原因排查详解

    目录 背景 临时性内存泄漏 通道理解 背景 Go 语言中有对应的Go 内存回收机制,在Go采用 并发三色标记清除  算法, 但是由于实际的过程中 发现会有一些内存泄漏的常见,内存泄漏 分为: 临时性 和 永久性内存泄漏. 初步排查过程中: 发现Linux使用top 发现内存随着时间会持续的增加没有稳定在一个合理值中. 在使用 pprof ,BBC 等 Go的内存泄漏工具进行排查 临时性内存泄漏 指的释放内存 不及时,对应的内存在更晚时候释放,这类问题主要是 string,slice 和底层的Bu

  • nodeJs内存泄漏问题详解

    之前一次偶然机会发现,react 在server渲染时,当NODE_ENV != production时,会导致内存泄漏.具体issues: https://github.com/facebook/react/issues/7406 .随着node,react同构等技术地广泛运用,node端内存泄漏等问题应该引起我们的重视.为什么node容易出现内存泄漏以及出现之后应该如何排查,下面通过一个简单的介绍以及例子来说明. 首先,node是基于v8引擎基础上,其内存管理方式与v8一致.下面简单介绍v8

随机推荐