js内存泄漏分析

1,127 阅读7分钟

背景
某日上线了一个视频16路播放功能,第二天...booooom
“产品同学A”: 这个页面停留个10分钟,浏览器就直接卡爆了呀,什么情况?
“开发同学B”: chrome内存一直飙升啊,赶紧看看?
“业务C”: 功能完全不可用啊?

得了,赶紧一阵分析,最终,识别是在请求拦截时存在内存泄漏,正好借此机会,系统了解下js内存泄漏分析工具和方法吧。

浏览器内存分析工具

Chrome任务管理器

点击右上角工具菜单->更多工具->任务管理器

企业微信截图_9dca2249-9cde-4815-9e58-ec8a4689e5e7.png

image.png

此处两个比较重要指标如下:

内存占用空间:使用的是本机内存,DOM节点相关内存都存储在本机内存中,该内存浏览器并非进程独享,比如说GPU进程要加速某个标签进程中的DOM渲染,就需要获取该DOM树内存
javascript使用的内存: 是当前标签进程JavaScript独享的内存,用于存储JavaScript运行中的相关对象,这一列涉及到两个值,一个是对象值,一个是实际对象值

  • 对象值比较好理解,就是当前JavaScript运行当中,所有对象占用内存的总和
  • 实际对象值,底层上来讲内存中依旧存在对该对象的引用,没有被GC标记为清楚额对象都为实际对象

但在上述bug定位过程中,发现任务管理器内存占用空间一直增加,但js使用内存未明显增加,且综合最终bug定位是fetch拦截导致的内存泄漏,推测fetch请求拦截不会占用javascript内存,而是使用本机内存。

image.png

image.png javascript使用内存在36-38M之间波动,然而内存占用空间逐步上涨至2G左右

下面结合浏览器调试工具进行具体分析

Chrome Performace

打开控制台上的Performace面板 ,勾选Memory选项,开始录制前先点击垃圾回收-->点击开始录制-->点击垃圾回收-->点击结束录制

image.png

image.png 浏览器自动回收机制会进行垃圾回收,因此曲线会有升有降

Chrome Memory

我们可以通过memory拍摄一个堆快照,查看具体内存占用情况

summary总览视图

image.png image.png

image.png 上面进行内存具体分析时:

  • Contructor - 表示使用此构造函数创建的所有对象
  • Distance - 显示使用节点最短简单路径时距根节点的距离,又称为可达性
  • Shallow Size - 显示通过特定构造函数创建的所有对象浅层大小的总和。浅层大小是指对象自身占用的内存大小(一般来说,数组和字符串的浅层大小比较大)
  • Retained Size - 显示同一组对象中最大的保留大小。对象的最大保留内存,保留内存是指对象被删除后可以释放的那部分内存。

可达性(Reachability)
在 JavaScript 中,可达性指的是一个变量是否能够直接或间接通过全局对象访问到,如果可以那么该变量就是可达的(Reachable),否则就是不可达的(Unreachable)。

上图中的节点 9 和节点 10 均无法通过节点 1(根节点)直接或间接访问,所以它们都是不可达的,可以被安全地回收。

快照可以用来发现DOM泄露。在Class filter(类过滤器)文本框中输入Detached可以搜索分离的DOM树。如果分离节点被JS引用,有可能就是泄露点。
Detached树表示,一个DOM节点只有在没有被页面的DOM树或者Javascript引用时,才会被垃圾回收。当一个节点处于“detached”状态,表示它已经不在DOM树上了,但Javascript仍旧对它有引用,所以暂时没有被回收。通常,Detached DOM tree往往会造成内存泄漏,我们可以重点分析这部分的数据。

对比分析:comparison视图

可以录制多个内存快照,进行对比分析

image.png

  • #New - Comparison 特有 - 新增项
  • #Deleted - Comparison 特有 - 删除项
  • #Delta - Comparison 特有 - 增量
  • Alloc. Size - Comparison 特有 - 内存分配大小
  • Freed Size - Comparison 特有 - 释放大小
  • Size Delta - Comparison 特有 - 内存增量

常见内存泄漏场景

  1. console导致的内存泄漏。
  2. 被遗忘的定时器 例如在组件初始化的时候设置了setInterval,那么在组件销毁之前记得调用clearInterval方法取消定时器。
  3. 没有正确移除事件监听器(各种EventBus, dom事件监听等)特征:performance里,监听器数量会持续上升

垃圾回收机制

由于栈内存由操作系统直接管理,所以当我们提到 GC 时指的都是堆内存的垃圾回收。
基本上现在的浏览器的 JavaScript 引擎(如 V8 和 SpiderMonkey)都实现了垃圾回收机制,引擎中的垃圾回收器(Garbage collector)会定期进行垃圾回收。

标记-清除(Mark-and-Sweep)

标记清除算法是目前最常用的垃圾收集算法之一。
当一个变量进入执行上下文时,它就会被标记为“处于上下文中”;而当变量离开执行上下文时,则会被标记为“已离开上下文”。
垃圾回收器将定期扫描内存中的所有变量,将处于上下文中以及被处于上下文中的变量引用的变量的标记去除,将其余变量标记为“待删除”。
随后,垃圾回收器会清除所有带有“待删除”标记的变量,并释放它们所占用的内存。 需要注意的是,这个算法的效率不算高,同时会引起内存碎片化的问题。

标记-整理(Mark-Compact)

使用标记整理算法可以解决内存碎片化的问题(通过整理),提高内存空间的可用性。
但是,该算法的标记阶段比较耗时,可能会堵塞主线程,导致程序长时间处于无响应状态。 虽然算法的名字上只有标记和整理,但这个算法通常有 3 个阶段,即标记整理清除
 首先,在标记阶段,垃圾回收器会从全局对象(根)开始,一层一层往下查询,直到标记完所有活跃的对象,那么剩下的未被标记的对象就是不可达的了。

然后是整理阶段(碎片整理),垃圾回收器会将活跃的(被标记了的)对象往内存空间的一端移动,这个过程可能会改变内存中的对象的内存地址。 最后来到清除阶段,垃圾回收器会将边界后面(也就是最后一个活跃的对象后面)的对象清除,并释放它们占用的内存空间。

引用计数(Reference counting)

引用计数算法是基于“引用计数”实现的垃圾回收算法,这是最初级但已经被弃用的垃圾回收算法。

引用计数算法需要 JavaScript 引擎在程序运行时记录每个变量被引用的次数,随后根据引用的次数来判断变量是否能够被回收。

bug: 循环引用(Circular references)

引用计数算法看似很美好,但是它有一个致命的缺点,就是无法处理循环引用的情况。

在下方的例子中,当 foo() 函数执行完毕之后,对象 a 与 b 都已经离开了作用域,理论上它们都应该能够被回收才对。

但是由于它们互相引用了对方,所以垃圾回收器就认为他们都还在被引用着,导致它们哥俩永远都不会被回收,这就造成了内存泄露

function foo() {
  let a = { o: null };
  let b = { o: null };
  a.o = b;
  b.o = a;
}
foo();

参考资料

zhuanlan.zhihu.com/p/343983256