内存泄漏
系统进程不再用到的内存没有释放,就叫做内存泄漏(memory leak)。当内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
目前没有找到能够完美检测内存泄漏的工具和排查方法(尤其是大项目),因此本文主要提供一些内存泄漏的可能原因,以及常用的一些排查方法。
垃圾回收机制
浏览器的垃圾回收机制主要有标记清理和引用计数两种,目前绝大多数都是标记清除。
要注意大多数情况下垃圾回收都不是实时的,浏览器更倾向于在程序比较”清闲”的时候进行这个过程。
标记清除
如下图所示,触发garbage collect 时,会遍历数据,然后将无法访问的数据当作垃圾回收
引用计数
另一种不常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被引用的次数。
声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0 的值的内存。
不过引用计数有非常大的问题:如果是互相引用,次数也会叠加,如下面的函数,变量a会被记成2次引用,即使函数执行完了,a也不会被当成垃圾回收。
function fn(){
var a=1
var b=a
}
fn()
复制代码
主要原因
全局变量
在非严格模式下当引用未声明的变量时,会在全局对象中创建一个新变量。在浏览器中,全局对象将是window。
function foo (arg) {
bar = "some text"; // bar将泄漏到全局.
}
复制代码
一般可以通过严格模式或者代码校验来解决(如eslint)
console与之类似,console打印的信息可以在控制台查看,其输出的对象不会被释放。可以利用webpack,code-review,eslint等方式避免无用的console语句。
闭包
闭包可能是内存泄漏最大的嫌疑犯,其定义如下:
声明在函数内部,可以访问函数的局部变量的函数
1.可以读取函数内部的变量。
2.让这些变量的值始终保持在内存中。
var count=10;
function add(){
var count=0;
function inner (){
count+=1;
alert(count);
}
return inner
}
var s=add();
s();//输出1
s();//输出2
复制代码
原因:闭包可以读取函数内部的变量,然后让这些变量始终保存在内存中。如果在使用结束后没有将局部变量清除,就可能导致内存泄露。
注意: 闭包本身没有错,不会引起内存泄漏.而是使用错误导致.
没有清理的 DOM 元素引用
原因:虽然别的地方删除了,但是对象中还存在对 dom 的引用。
// 在对象中引用DOM
var elements = {
btn: document.getElementById('btn'),
}
function doSomeThing() {
elements.btn.click()
}
function removeBtn() {
// 将body中的btn移除, 也就是移除 DOM树中的btn
document.body.removeChild(document.getElementById('button'))
// 但是此时全局变量elements还是保留了对btn的引用, btn还是存在于内存中,不能被GC回收
}
复制代码
除上述代码外,也要考虑如果在清除dom前有子元素被引用,需要先将子元素的引用释放,否则该dom还是会在内存中无法释放。
有一个虚拟列表的组件在1.x版本就有该问题,感兴趣的可以看一下:github.com/tangbc/vue-…
被遗忘的定时器或者回调
定时器中有 dom 的引用,即使 dom 删除了,但是定时器还在,所以内存中还是有这个 dom。
以及组件已经销毁,但是定时器还在导致代码报错堵塞,也有可能会造成内存泄漏(不过这个就很明显了,毕竟控制台都报错了)
监听事件没有解绑
window.addEventListener 之类的时间监听,绑在 EventBus 的事件.
例如在组件初始化时绑定监听,但是销毁时没有解绑,这样再次初始化时就会重复绑定监听,导致内存不断泄漏。
不当的代码操作
例如(我见过的):
- 对于一个数组只有push而没有清理,导致该数组占用不断增大,但保存的基本都是废弃信息
- (Vue)destroy拼写错误导致节点无法销毁
排查方法
工具
chrome Memory
主要用到两种模式:
- Heap snapshot – 用以打印堆快照,堆快照文件显示页面的 javascript 对象和相关 DOM 节点之间的内存分配
- Allocation instrumentation on timeline – 在时间轴上记录内存信息,随着时间变化记录内存信息。
Heap snapshot
一般用于在循环操作的不同次数后录制快照,然后进行比对。点击快照会显示当前快照的信息,点击左上角模式可以切换为对比模式,与其他的快照进行比较。
点开每个变量,提供引用链retainers,会说明该变量被哪里引用。
需要注意的一点是Total JS heap size显示数值和Heap snapshot生成快照数值可能会有所不同,原因是(chrome)在调用快照时会自动触发一次gc,所以生成快照后数值和之前显示的统计值有差异。
Allocation instrumentation on timeline
图中最为明显的信息为时间轴,记录了随着时间推移系统对内存的申请情况。每一根柱条都代表内存空间,灰色的代表已经释放,蓝色的代表仍在占用。
可以细化时间轴的范围,只显示某一次循环的内存情况。
由图显而易见,在两个页面状态之间循环切换,每次切换都有没有释放的内存,说明有内存泄漏的情况。
注意:由于该工具会占用较大性能,导致页面变卡。
判断是否存在内存泄漏
- 观察相邻的snapshot快照(项目较大时不好判断,且干扰很多)
- 观察内存时间轴是否在循环操作的过程中每次都有未释放的内存
- 使用脚本模拟长时间操作,观察内存占用数值(需要手动触发GC防止误判)
注意:这个过程需要在线上环境进行,因为开发环境由于一些未知原因(可能是webpack热更新引用导致)在某些情况会出现内存泄漏,但实际上发布后并没有这个问题。(例如打包后使用http-server模拟线上环境进行验证)
如何解决内存泄漏
内存泄漏的原因五花八门,没有万能的解决办法,但只要定位了原因,就能够针对性的进行修复(一般来说不会有结构性的问题),这个优化的主要难点也就是在定位上,往往是花数小时定位,然后花数分钟解决。
- 观察内存记录看是否为已知的数据结构,是否经常出现某个组件或函数
- 观察控制台看是否有报错或警告信息
- 观察排查模块是否有定时或防抖函数设置,是否有绑定监听事件等
- 暴力定位(注释二分法)
一般来说对项目代码越熟悉,就能越快的从海量的信息中找到问题所在的位置,但有时候也可能被一些无用的信息误导导致走一堆弯路,所以对代码完全不熟悉或者光是看引用链找不到问题的时候,暴力定位也不是不能接受————最起码好过钻牛角尖。
实例分析
遇到的实例都是公司代码这里就不放上来了,简单模拟也没啥代表性,干扰信息量和代码复杂度成正比,有的帖子给的简单实例感觉并没有什么实际作用,而复杂代码也不是那么好造数据。
最后这里提一句,有时候内存泄漏的问题不一定是自己导致的,第三方库也很有可能造成这个问题,jquery UI和element UI都遇到过这个问题(github.com/ElemeFE/ele…
总结
在 Web 应用中查找和修复内存泄漏的状态仍然很初级。在本文中介绍了一些可能有用的技术和实例,但是这仍然是一个困难且耗时的过程。
与大多数性能问题一样,少量预防胜过大量的治疗。进行综合测试是值得的,而不是在事实发生后尝试调试内存泄漏。尤其是如果页面上存在多个泄漏,则可能会变成洋葱剥皮练习——你先修复一个泄漏,然后查找另一个泄漏,然后重复(整个过程都在哭泣!)。
当然,如果能在代码编写阶段就进行预防,那就更好了。
(这段总结不是我写的,但是在看到之后感同身受,就厚着脸皮引用了一下)