JavaScript 内存泄漏排查方法_js内存泄漏及解决方法
itomcoil 2025-09-28 01:19 2 浏览
一、概述
本文主要介绍了如何通过 Devtools 的 Memory 内存工具排查 JavaScript 内存泄漏问题。先介绍了一些相关概念,说明了 Memory 内存工具的使用方式,然后介绍了堆快照的分析方式,说明如何通过分析堆快照找到泄漏的 JavaScript 代码,最后列举了一些 JavaScript 内存泄漏的排查案例。
二、概念说明
1、内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢,甚至系统崩溃等严重后果。
简单来说就是,按照业务逻辑,本该被回收的对象,可能因为某些代码的实现不合理,导致对象没有被及时回收,进而对象占用的内存无法释放,导致内存的浪费。
2、Memory 常用功能
- 快照查看方式:主要有摘要和对比
- 类筛选器:可以过滤构造函数,但是不能过滤对象名
- 例如,要查看包含video的构造函数名:
3、堆快照视图
3.1 摘要视图
- 构造函数:JS构造函数,以及由JS引擎、框架或库创建的构造函数
- 距离 (distance):与 GC root 之间的距离,如果某节点没有 distance,通常说明该节点即将被 gc 回收
- 卷影大小/浅层大小 (shadow size):对象本身的大小
- 浅层大小可以直观地看出内存具体地分配给哪些对象了
- 保留的大小 (retained size):对象释放后可以回收的内存大小(参考chrome的定义)
- 保留的大小说明了哪些对象导致了内存占用高,但不一定是这个对象本身内存高,可能是因为它引用的对象占用内存高。
- 有时候某个对象的 retained size,不等于其所有属性的 retained size 之和。因为该对象的多个属性都被回收之后,才能让这多个属性引用的对象回收,所以这些被引用的对象的大小不会计入此对象中。例如这里的 DOMTimer@1199111936,保留的大小为 1468B ,但其引用的一个context@2160207 保留的大小为 124520B,已经大于了 DOMTimer 的保留大小了。因为这个context不止被这个 DOMTimer 引用,还被其他的 DOMTimer 引用,需要这些 DOMTimer 全部被回收之后,才能把这个 context 回收,因此它的大小不计入 DOMTimer 中。换句话说,如果只有 DOMTimer 引用这个 context,那 context 的保留大小就会计入 DOMTimer 中。
3.2 对比视图
- 新建:快照查看方式选择比较后,若是新建列中有一个点,则表示是在两个快照之间新建的对象
- 已删除:同理,若在已删除列有一个点,表示在两个快照之间删除的对象
- 增量:指对象增加的数量
- 分配大小:在两个快照之间分配的大小
- 已释放:在两个快照之间释放的内存大小
- 大小增量:在两个快照之间增长的浅层大小
4、构造函数和对象
4.1 构造函数
- 在上半部分的构造函数这一列中,第一层都是构造函数,后面的数字表示这个类的对象数。例如下图中,当前的Object数量为89160
- 展开某一个对象后,子元素表示该对象的属性,例如这里Object@207683的属性包含aweme_list、map等
- 对象名的::之后的即对象所属的类,@符号之后的表示对象id
4.2 对象
在下半部分的对象这一列中,节点之间的关系为:当前节点被子节点引用。例如前3行,aweme_list被一个Object类型的对象H引用,H被一个Context类型的对象context引用,context被一个函数类型的对象get $引用。
蓝色的链接可以跳转到源代码,源代码中会以下划线标注某段代码:
表示在这段代码中,当前对象引用了下一个对象。例如下图中的4784.0ec58630.js:1的某段代码中,$()函数的上下文context引用了H
4.3 查看对象信息
通过鼠标悬停在对象上,可以查看对象信息,不过并不是所有对象都能查看到信息,显示"预览不可用"的对象可能已经被回收了。
4.4 在页面访问 dom
- 如果对象的右边有个窗口图标,则表示可以在窗口访问这个元素,鼠标悬停在对象上,可以查看信息,同时会在页面高亮该对象,例如这个video标签是当前视频所在video标签
- 有时候即使有这个窗口图标,页面中也不会高亮这个元素(可能已经是Detached状态了),或者有些元素没有这个窗口图标。这个时候如果还想知道这个是什么元素,可以查看其信息,找到其对应的class,然后在“元素”中搜索
- 例如这里的Detached HTMLImageElement@2407721对象,它实际上就是页面中的“抢”标签:
- 如果查看元素对象的信息时,显示"预览不可用",则暂时没有办法找到该元素。此时可以看看它引用了哪些元素或者被哪些元素引用,看看是否能在页面中查看这些元素,如果可以,再以此推测之前的元素。
5、堆快照常见对象类型
5.1 Detached DOM
- 如果删除了某个dom节点,但仍有变量对此节点存在引用关系,则这个dom节点就会变成游离状态,也就是不存在于document上了。
- 简单来说就是,dom节点已经不存在于页面中了,但仍然被JS对象引用着。
5.2 DOM Timer
定时器。setInterval()和setTimeout()函数会创建。是最容易出现泄漏的对象之一,写代码时,很容易出现创建了定时器但是没有销毁的情况,这样就会导致定时器引用的对象泄漏。
5.3 Context
通常指函数的上下文
- 例如以下代码中,会自动为inlineTestFunc函数创建一个context对象,该context对象会引用variable
function testFunc(){
const variable = 'I am refereced by inlineTestFunc()'
const inlineTestFunc = function () {}
return inlineTestFunc
}
window.testFunc = testFunc()
- 如果在其他地方引用了inlineTestFunc()函数,那么variable变量也会同时被引用。
5.4 Closure
闭包:函数以及其捆绑的周边环境状态
5.5 Compiled code
运行代码占用的内存,通常不会出现内存泄漏
5.6 InternalNode
浏览器内置对象,通常不需要关注,一方面是因为导致内存泄漏的一般是JS对象,而不是内部对象。另一方面是因为它造成的内存泄漏在前端不好解决。如果确实需要获得InternalNode的具体对象名,来排查内存泄漏,可以通过在编译chrome时添加特定参数来实现,可以参考:
- InternalNode是什么:
- https://link.juejin.cn/?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F66802111%2Fwhat-is-internalnode-in-chrome-heap-profile
- 如何编译Chrome:
- https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fchromium%2Fchromium%2Fblob%2Fmaster%2Fdocs%2Fwindows_build_instructions.md
三、排查步骤
1、Devtools手动排查
使用 Devtools 的 Memory 内存工具来对 JS 内存泄漏进行排查分析
1.1 复现和堆快照抓取
- 确定疑似有内存泄漏的操作,例如抖音PC客户端中切换视频、发送评论、发送弹幕等。复现操作要尽可能的小,并且最好尽可能地排除其他变量的干扰,这对后续的问题定位有很大的影响。
- 访问不压缩代码的页面(可选)
- 手动或用js脚本写 puppeteer 在浏览器复现
- GC 后拍摄快照;执行疑似导致内存泄漏的操作;GC 后拍摄快照
- a. 执行疑似泄漏的操作时,建议重复执行多次,让上涨的内存大于 30MB 以上,根据过往经验,最好在100MB~200MB左右(太大的话抓快照太慢了),这样会比较容易观察,否则上涨的内存太小,不容易发现是哪些对象增长。通常情况下,这个快照大小几乎可以直接看出哪些对象异常。例如这里切换了 50 个视频,可以明显地看出 Video 元素的异常:
1.2 筛选泄漏的引用链
比较执行泄漏操作前和执行泄漏操作后的快照,筛选疑似泄漏的引用链
1.2.1 筛选方法
手动筛选时,往往很难 100% 确定哪些对象是泄漏的,一般来说只能是怀疑某些对象泄漏,有了怀疑的对象后,按照下一步的方法来分析引用链是否合理,如果没有找到可疑的引用链,那么就需要反复进行1.2和1.3步骤,才能更容易找到泄漏的对象。如果说看了好多对象,或者看了几分钟都没看出来有哪些异常,那么可能是测试得到堆快照对比的增量大小太小了,不容易直接看出来,或者是观察的数据类型不好找到泄漏,比如Object、Array就很难筛选出泄漏的对象,这时可以考虑看其他数据类型。
1.2.2 优先看内存增量大的对象
优先看内存增量比较高的数据,例如某些对象的大小增量为 100M,而其他的就只有 10M 不到,那么优先看大小增量为 100M 的。如果都内存增长都差不多,可以继续按下面步骤进行排查。
如果某些对象大小增量比较高,说明它们最有可能是泄漏的对象。它们为什么没有被回收?可以通过点开对象查看引用的信息,分析该对象整个引用跟踪链路,来找到其中某个不合理的引用。
1.2.3 注意内存占用大的 Detached 元素
如果没有内存占用相对较高的对象,或者有好几种数据大小增长都差不多,可以从Detached元素入手,Detached元素出现内存泄漏的概率比较大,可以观察内存占用相对比较高的Detached元素,原因有两个:
这里需要注意的是,Detached元素的浅层大小通常是很小的,而前面提到过,通过堆快照对比得出的“大小增量”指的是浅层大小增量,所以在堆快照的对比视图里,Detached元素的大小增量一般都会比较小,它实际造成的泄漏是大于“大小增量”这个值的,那么怎么知道它造成了多少泄漏?可以点开某个元素,可以看到它的保留的大小,这个就反应出了它造成的内存泄漏大小。
- 一方面是因为object、array之类的数据,通常对象数量非常大(几万个是比较常见的),并且有很多是正常对象,而泄漏的对象混杂在其中很难分辨哪些是泄漏的、哪些是正常的,除非泄漏的对象非常多,占据它们数量的大部分,或者有个别对象占用的内存特别大时,才比较容易直接观察到。
- 另一方面是Detached元素,通常对象数较少,并且比较容易出现相似的引用链,如果这里面有新的泄漏对象出现,很容易发现这些新增的泄漏对象
1.2.4 注意常见泄漏类型
事件监听(EventListener)、定时器(DOMTimer)、数组(Array),这是最容易导致内存泄漏的几种数据类型,比如监听事件之后没有及时取消监听,定时器开启之后没有销毁,数组元素无限增加,可以专门针对这几种类型进行排查,并对用到这类对象的代码格外注意。
- 可通过多次复现泄漏操作的方式来确定某些对象或者数组是否存在泄漏。先抓取快照,鼠标悬停到疑似泄漏的数组上,查看信息,记录下数组长度,然后执行一遍疑似导致内存泄漏的操作,再查看数组长度(这里查看到的对象信息是实时的,所以无需抓快照也能看到当前的数组详情),如果数组增长了,那么就有可能是泄漏的,接下来可以多重复几次导致泄漏的操作,看看是否按预期增长,如果按预期增长,那么基本可以确定是泄漏对象。(为了确保准确性,可以在记录长度前先进行 GC)
- 下图中通过查看数组信息,可以看到数组长度为 986,执行一次疑似泄漏操作后,长度变为了 989。因此推测这里每执行一次疑似泄漏操作会 +3 个元素,所以推测执行 10 次疑似泄漏操作后,会增加 30 个,这里经过验证确实是增加 30 个,并且经过一段时间后,依旧没有减少,所以基本可以确定这个数组为泄漏对象
1.2.5 注意频繁出现的引用链
内存泄漏通常会引起很多类似的对象无法被销毁,因此很容易会出现很多对象的引用链是一样的,所以可以在点开某种数据类型后(如DOM元素、object、string),多观察几个对象的引用链,如果某一条类似的引用链频繁出现,那么很有可能该引用链中出现了泄漏。
- 例如下面的图中,10 个Object对象出现了 8 个相同的引用链,因此这条引用链中很可能存在泄漏,通过不断测试可以确定引用链中的InternalNode是无限增长的,到此可以认为这里是存在内存泄漏的,再对HTMLVideoElement进行分析,最终确定是MediaElement和VideoElement存在内存泄漏。
1.3 确定泄漏的对象和代码
在上一步骤中,筛选出一些疑似泄漏的引用链后,开始分析引用链的泄漏对象,确定导致泄漏的某个引用。
通常可以先不看InternalNode对象
1.3.1 从业务逻辑分析哪些是泄漏对象
根据业务逻辑来分析哪些对象是不应该存在的,查找创建或者引用了这个对象的代码片段。可以先搞清楚整个引用链存在的原因,通过引用链上的对象的信息,或者dom元素信息等,来分析这个引用链因为哪一个业务逻辑而存在,根据这个业务逻辑联想可能的泄漏情况,比如常见的事件、定时器是否已清除。
- 例如下图这个引用链,从observerList和domResizeListener这两个对象名可以推测这里使用了监听器模式,domResizeListener是监听器,当改变页面大小的时候,通过调用observerList里的观察者的回调函数,修改某些 dom 的大小。根据这个代码逻辑联想,怀疑某些地方把一些 dom 元素加入到observerList里了,但是 dom 销毁的时候没有取消监听,dom 元素依然存在observerList里,导致无法被回收。
1.3.2 导致泄漏的引用通常“距离”较大
- 导致泄漏的引用比较容易发生在“距离”较大的地方,也就是距离根节点比较远的对象(DOMTimer除外)。距离根节点较远的对象,和业务代码的相关性比较大,距离根节点较近的对象,大多都是一些常驻对象,或者是难以回收的对象;而“距离”较大的对象,往往只会被一个对象引用,因为“距离”大的对象往往是业务代码创建的,而“距离”较小的对象,通常会被很多对象引用,或者是一些底层的框架之类的,往往不容易出现泄漏。
- a. 例如下图中,上方的array为泄漏对象,为了避免这个泄漏,从根节点config到data这条引用链路中,回收任意一个对象都能使array被回收,但是如果要回收config,就需要把引用它的player、bound_this、context等都回收才行,一般来说这样距离根节点较近的对象是很难回收的,通常也不是造成泄漏的原因,而这里最终泄漏的原因是,array是缓存的数据,把缓存放进去之后没有及时清理。(引用链中只能看出这个数组被谁引用,看不出是哪里添加的数据,需要根据业务逻辑和代码进行分析)
1.3.3 定位引用所在代码
在对象视图中的代码跳转链接,指出了对象被引用的地方,但并不是说就是这一行代码导致的内存泄漏。例如下图中泄漏的对象是taskCallback,通过代码跳转,指出的代码是a.taskCallback(),说明taskCallback因为a对象的引用而无法被回收。这里经过代码分析得出的结论是:this.intervalTimer没有及时销毁,继而存在引用intervalTimer->a->taskCallback,而导致taskCallback函数及其引用的对象泄漏。
四、排查案例
1、监听器泄漏
- 在抖音PC客户端中,通过自动化测试发现,刷视频会出现持续的内存上涨,所以针对刷视频这个场景进行内存泄漏排查。在刷视频之前和刷视频之后,分别抓取内存快照,选择“比较”对这两个快照进行比较,搜索detached元素,选中一个div查看其引用链
- 引用链表示从引用该div的对象出发,一直到根节点的整个链路。
- 通过观察多个div发现,大部分都存在相同的引用链,即和上图类似。detached元素本身就很可能是泄漏的对象,加上很多detached元素都有相同的引用链,所以这个引用链很可能存在内存泄漏。通过分析这些引用的名字,可以推测使用了监听器,JS代码中比较容易出现泄漏的情况有监听器、定时器,这里怀疑是showDisturbLoginPanel这个对象,它被_events引用,推测这是一个事件,通过鼠标悬停到_events上查看内存,showDisturbLoginPanel是一个数组,里面存放的是函数:
- 展开函数,可以看到函数的代码位置。查看了好几个函数,发现这些函数的位置都是相同的,点开函数:
- 可以看到是监听事件,由此初步推测:每次刷视频,都会监听事件,但是没有及时销毁。
- 有了初步推测之后,接下来再复现一次内存泄漏,然后验证结果是否符合推测:
- 可以看到,再刷一次视频后,数组长度 +3,到这里基本可以确定:showDisturbLoginPanel中引用的函数没有释放而引起泄漏,即事件没有及时销毁。
相关推荐
- 《Queendom》宣布冠军!女团MAMAMOO四人激动落泪
-
网易娱乐11月1日报道据台湾媒体报道,南韩女团竞争回归的生死斗《Queendom》昨(10/31)晚播出大决赛,并以直播方式进行,6组女团、女歌手皆演唱新歌,并加总前三轮的赛前赛、音源成绩与直播现场投...
- 正确复制、重写别人的代码,不算抄袭
-
我最近在一篇文章提到,工程师应该怎样避免使用大量的库、包以及其他依赖关系。我建议的另一种方案是,如果你没有达到重用第三方代码的阈值时,那么你就可以自己编写代码。在本文中,我将讨论一个在重用和从头开始编...
- HTML DOM tr 对象_html event对象
-
tr对象tr对象代表了HTML表格的行。HTML文档中出现一个<tr>标签,就会创建一个tr对象。tr对象集合W3C:W3C标签。集合描述W3Ccells返回...
- JS 打造动态表格_js如何动态改变表格内容
-
后台列表页最常见的需求:点击表头排序+一键全选。本文用原生js代码实现零依赖方案,涵盖DOM查询、排序算法、事件代理三大核心技能。效果速览一、核心思路事件入口:为每个<th>绑...
- 连肝7个晚上,总结了66条计算机网络的知识点
-
作者|哪吒来源|程序员小灰(ID:chengxuyuanxiaohui)计算机网络知识是面试常考的内容,在实际工作中经常涉及。最近,我总结了66条计算机网络相关的知识点。1、比较http0....
- Vue 中 强制组件重新渲染的正确方法
-
作者:MichaelThiessen译者:前端小智来源:hackernoon有时候,依赖Vue响应方式来更新数据是不够的,相反,我们需要手动重新渲染组件来更新数据。或者,我们可能只想抛开当前的...
- 为什么100个前端只有1人能说清?浏览器重排/重绘深度解析
-
面试现场的"致命拷问""你的项目里做过哪些性能优化?能具体讲讲重排和重绘的区别吗?"作为面试官,我在秋招季连续面试过100多位前端候选人,这句提问几乎成了必考题。但令...
- HTML DOM 介绍_dom4j html
-
HTMLDOM(文档对象模型)是一种基于文档的编程接口,它是HTML和XML文档的编程接口。它可以让开发人员通过JavaScript或其他脚本语言来访问和操作HTML和XML文档...
- JavaScript 事件——“事件流和事件处理程序”的注意要点
-
事件流事件流描述的是从页面中接收事件的顺序。IE的事件流是事件冒泡流,而NetscapeCommunicator的事件流是事件捕获流。事件冒泡即事件开始时由最具体的元素接收,然后逐级向上传播到较为不...
- 探秘 Web 水印技术_水印制作网页
-
作者:fransli,腾讯PCG前端开发工程师Web水印技术在信息安全和版权保护等领域有着广泛的应用,对防止信息泄露或知识产品被侵犯有重要意义。水印根据可见性可分为可见水印和不可见水印(盲水印)...
- 国外顶流网红为流量拍摄性侵女学生?仅被封杀三月,回归仍爆火
-
曾经的油管之王,顶流网红DavidDobrik复出了。一切似乎都跟他因和成员灌酒性侵女学生被骂到退网之前一样:住在950万美元的豪宅,开着20万美元的阿斯顿马丁,每条视频都有数百万观看...人们仿佛...
- JavaScript 内存泄漏排查方法_js内存泄漏及解决方法
-
一、概述本文主要介绍了如何通过Devtools的Memory内存工具排查JavaScript内存泄漏问题。先介绍了一些相关概念,说明了Memory内存工具的使用方式,然后介绍了堆快照的...
- 外贸独立站,网站优化的具体内容_外贸独立站,网站优化的具体内容有哪些
-
Wordpress网站优化,是通过优化代码、数据库、缓存、CSS/JS等内容,提升网站加载速度、交互性和稳定性。网站加载速度,是Google搜索引擎的第一权重,也是SEO优化的前提。1.优化渲染阻塞。...
- 这8个CSS工具可以提升编程速度_css用什么编译器
-
下面为大家推荐的这8个CSS工具,有提供函数的,有提供类的,有提取代码的,还有收集CSS的统计数据的……请花费两分钟的时间看完这篇文章,或许你会找到意外的惊喜,并且为你的编程之路打开了一扇新的大门。1...
- vue的理解-vue源码 历史 简介 核心特性 和jquery区别 和 react对比
-
一、从历史说起Web是WorldWideWeb的简称,中文译为万维网我们可以将它规划成如下的几个时代来进行理解石器时代文明时代工业革命时代百花齐放时代石器时代石器时代指的就是我们的静态网页,可以欣...
- 一周热门
- 最近发表
- 标签列表
-
- ps图案在哪里 (33)
- super().__init__ (33)
- python 获取日期 (34)
- 0xa (36)
- super().__init__()详解 (33)
- python安装包在哪里找 (33)
- linux查看python版本信息 (35)
- python怎么改成中文 (35)
- php文件怎么在浏览器运行 (33)
- eval在python中的意思 (33)
- python安装opencv库 (35)
- python div (34)
- sticky css (33)
- python中random.randint()函数 (34)
- python去掉字符串中的指定字符 (33)
- python入门经典100题 (34)
- anaconda安装路径 (34)
- yield和return的区别 (33)
- 1到10的阶乘之和是多少 (35)
- python安装sklearn库 (33)
- dom和bom区别 (33)
- js 替换指定位置的字符 (33)
- python判断元素是否存在 (33)
- sorted key (33)
- shutil.copy() (33)