一. 何为内存泄漏
在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
在浏览器中,常见的内存泄漏包含以下场景:
-
意外的全局变量 函数中意外的定义了全局变量,每次执行该函数都会生成该变量,且不会随着函数执行结束而释放。
-
未清除的定时器 定时器没有清除,它内部引用的变量,不会被释放
-
脱离DOM的元素引用 一个dom容器删除之后,变量未置为null,则其内部的dom元素则不会释放。通过Memory的快照,可以看到detached的dom元素,即为脱离dom的元素引用
-
闭包引起内存泄漏
-
持续绑定的事件 函数中addEventListener绑定事件,函数多次执行,绑定便会产生多次,产生内存泄漏。
二. 怎么分析某个流程是否存在内存泄露
举例:
- 进入采购入库列表页
- 进入编辑页
- 打开产品选择器添加新产品
- 任意编辑产品的价格数据
- 点击保存跳转到详情页面
- 点击菜单栏返回列表页面
使用了 Performance monitior 实时查看内存占用,
在执行完一组用户操作后, 清除console, network, 手动执行 GC 操作,观察内存增长情况;
如果每次执行完, 内存都会增长, 则表示出现了内存泄露
三. 分析工具
- performance monitor:实时监测
打开chrome面板--performace标签页,点击右上角 ...,选择more tools --> performance monitor
CPU usage - 当前站点的 CPU 使用量;
JS heap size - 应用的内存占用量;
DOM Nodes - 内存中 DOM 节点数目;
JS event listeners- 当前页面上注册的 JavaScript 事件监听器数量;
Documents - 当前页面中使用的样式或者脚本文件数目;
Frames - 当前页面上的 Frames 数目,包括 iframe 与 workers;
Layouts / sec - 每秒的 DOM 重布局数目;
Style recalcs / sec - 浏览器需要重新计算样式的频次;
- memory
通过快照, 查看实时内存情况。
shallow size:一个特定类型的所有对象的总和
retained size:shallow size 加上引用此对象的其它对象的大小。 也是指释放了该对象及其引用后,可以回收的内存。 即对象及其依赖对象的内存大小。
distance 显示了对象到达 GC 根(校者注:最初引用的那块内存)的最短距离。
constructor:表示所有通过该构造函数生成的对象。
四. 常见的内存泄露示例
-
意外的全局变量. 变量未申明, 会直接挂在window上
function fn() { a = 'global variable' } fn()
- setTimeout setInterval清除
定时器不会自动清除
因此使用后就应该清除
最保险的应该在beforeDestroy钩子里面清除
-
如果在mounted/created 钩子中绑定了DOM/BOM 对象中的事件,需要在beforeDestroy 中做对应解绑处理。
DOM 0级事件
ele.onclick=function() {
ele.onclick=null; //此处是等于null时,绑定解除
}
DOM 2级事件
mounted () {
window.addEventListener('resize', this.onResize)
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize)
}
- vue中的eventbus,需要在beforeDestroy 中做对应解绑(off)处理。
mounted () {
this.EventBus.on('exitClassRoom',this.exitClassRoomHandle)
},
beforeDestroy () {
this.EventBus.off('exitClassRoom',this.exitClassRoomHandle)
}
- 如果在mounted/created 钩子中使用了第三方库初始化,需要在beforeDestroy 中做对应销毁处理。
示例:
// 如果第三方库提供了对应销毁的方法, 比如ALIOSS 提供的是destory方法
// 可以使用commonOSS.destroy()解决
- 合理使用keep-alive
当你用 keep-alive 包裹一个组件后,它的状态就会保留,因此就留在了内存里,切莫在整个路由页面上加上keep-alive。
一旦你使用了 keep-alive,那么你就可以访问另外两个生命周期钩子:activated和 deactivated。
你需要在一个 keep-alive 组件被移除的时候,调用 deactivated 钩子进行清理或改变数据
- dom元素的引用关系未被解除
// 在对象中引用DOM
var elements = { btn: document.getElementById('btn'), }
function doSomeThing() {
elements.btn.click()
}
function removeBtn() {
// 将body中的btn移除, 也就是移除 DOM树中的btn
// 但是此时全局变量elements还是保留了对btn的引用, btn还是存在于内存中,不能被GC回收 document.body.removeChild(document.getElementById('button'))
}
解决方法:手动删除,elements.btn = null。
- 使用WeakMap
下面以 WeakMap 为例,看看它是怎么解决内存泄漏的。
const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information"
上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。
也就是说,DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
五. 如何定位项目中内存泄露
- 快照对比
属性对应的具体含义如下:
重点关注delta增量
- 关注Detached HTMLDivElement
一个DOM节点只有在没有被页面的DOM树或者Javascript引用时,才会被垃圾回收。当一个节点处于“detached”状态,表示它已经不在DOM树上了,但Javascript仍旧对它有引用,所以暂时没有被回收。通常,Detached DOM tree往往会造成内存泄漏,我们可以重点分析这部分的数据
- 关注vue Component
组件未被卸载会造成内存泄露
- 对比快照需要更精确定位问题时可以一段一段地注释掉代码,分别打快照看看问题有没有消失。
如果消失了,说明问题就在刚被注释掉的这段代码之中
参考资料: