内存泄露分析终章

150 阅读6分钟

一. 何为内存泄漏

在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

在浏览器中,常见的内存泄漏包含以下场景:

  1. 意外的全局变量            函数中意外的定义了全局变量,每次执行该函数都会生成该变量,且不会随着函数执行结束而释放。

  2. 未清除的定时器           定时器没有清除,它内部引用的变量,不会被释放

  3. 脱离DOM的元素引用    一个dom容器删除之后,变量未置为null,则其内部的dom元素则不会释放。通过Memory的快照,可以看到detached的dom元素,即为脱离dom的元素引用

  4. 闭包引起内存泄漏

  5. 持续绑定的事件           函数中addEventListener绑定事件,函数多次执行,绑定便会产生多次,产生内存泄漏。

二. 怎么分析某个流程是否存在内存泄露

举例:

  • 进入采购入库列表页
  • 进入编辑页
  • 打开产品选择器添加新产品
  • 任意编辑产品的价格数据
  • 点击保存跳转到详情页面
  • 点击菜单栏返回列表页面

使用了 Performance monitior 实时查看内存占用,

在执行完一组用户操作后, 清除console, network, 手动执行 GC 操作,观察内存增长情况;

如果每次执行完, 内存都会增长, 则表示出现了内存泄露

三. 分析工具

  1. performance monitor:实时监测

打开chrome面板--performace标签页,点击右上角 ...,选择more tools --> performance monitor

1.png

CPU usage - 当前站点的 CPU 使用量;

JS heap size - 应用的内存占用量;

DOM Nodes - 内存中 DOM 节点数目;

JS event listeners- 当前页面上注册的 JavaScript 事件监听器数量;

Documents - 当前页面中使用的样式或者脚本文件数目;

Frames - 当前页面上的 Frames 数目,包括 iframe 与 workers;

Layouts / sec - 每秒的 DOM 重布局数目;

Style recalcs / sec - 浏览器需要重新计算样式的频次;

  1. memory

通过快照, 查看实时内存情况。

2.png

shallow size:一个特定类型的所有对象的总和

retained size:shallow size 加上引用此对象的其它对象的大小。 也是指释放了该对象及其引用后,可以回收的内存。 即对象及其依赖对象的内存大小。

distance 显示了对象到达 GC 根(校者注:最初引用的那块内存)的最短距离。

constructor:表示所有通过该构造函数生成的对象。

四. 常见的内存泄露示例

  1. 意外的全局变量. 变量未申明, 会直接挂在window上

function fn() { a = 'global variable' } fn()

  1. setTimeout setInterval清除

定时器不会自动清除

因此使用后就应该清除

最保险的应该在beforeDestroy钩子里面清除

  1. 如果在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)

}

  1. vue中的eventbus,需要在beforeDestroy 中做对应解绑(off)处理。

mounted () {

this.EventBus.on('exitClassRoom',this.exitClassRoomHandle)

},

beforeDestroy () {

this.EventBus.off('exitClassRoom',this.exitClassRoomHandle)

}

  1. 如果在mounted/created 钩子中使用了第三方库初始化,需要在beforeDestroy 中做对应销毁处理。

示例:

// 如果第三方库提供了对应销毁的方法, 比如ALIOSS 提供的是destory方法

// 可以使用commonOSS.destroy()解决

  1. 合理使用keep-alive

当你用 keep-alive 包裹一个组件后,它的状态就会保留,因此就留在了内存里,切莫在整个路由页面上加上keep-alive。

一旦你使用了 keep-alive,那么你就可以访问另外两个生命周期钩子:activated和 deactivated。

你需要在一个 keep-alive 组件被移除的时候,调用 deactivated 钩子进行清理或改变数据

  1. 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。

  1. 使用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。

五. 如何定位项目中内存泄露

  1. 快照对比

3.png

属性对应的具体含义如下:

重点关注delta增量

4.png

  1. 关注Detached HTMLDivElement

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

  1. 关注vue Component

组件未被卸载会造成内存泄露

  1. 对比快照需要更精确定位问题时可以一段一段地注释掉代码,分别打快照看看问题有没有消失。

如果消失了,说明问题就在刚被注释掉的这段代码之中

参考资料: