背景
项目是利用 vue 框架开发的公司内部的异常监控系统, 用于显示 java 程序运行时的异常信息, 包括执行堆栈, 代码, 变量等信息显示.
在测试过程中, 部门同事反映: 在不同的异常信息之间多次切换, 会导致网页崩溃. 在案发现场打开 Chrome 的任务管理器, 看到这个页面内存占用已经达到了 9.7G, 初步怀疑页面存在泄漏.
验证猜测
打开 devtool -> performance, 开始记录页面性能
执行页面上切换其它异常信息的操作(页面最有可能引起内存泄漏的操作)
查看性能分析结果
可以看到 Nodes,Listeners,JS Head(memory) 的阶梯式增长, 中间的增长节点对应就是每一次操作. 很显然这个操作会引起内存的持续增长, 最终发生内存溢出也是顺理成章了.
问题分析
在动手之前, 我已知的信息有:
从 performace 工具, 可以看到 JS Heap,Nodes,Listeners 的累积增长
以上三点, 实际上存在依赖关系:
变量引用 DOM
子级 DOM 不能释放, 会导致父级也不会被释放
如果 DOM 能被正常 GC, 对 DOM 的事件监听器也会自动移除
可以使用 devtool->memory -> take snapshot 收集内存快照并分析工具文档;
简单认识一下 snapshot
嗯, 内容有点多, 但是也还算清晰:
按数据类型进行统计, 可以看到一些内建对象, Vue 对象, 自定义对象(比如 Exception,StackFrame 等),Detached Element,EventListener 等等.
纵坐标有 Distance, shallow size, Retained Size, 可以不准确理解为:
Distance: 到 root 的引用距离
Shallow size: 对象本身的大小, 不包含它引用的数据的大小
Retained size: 对象自身以及所有引用的大小, 就是对象总共占用的内存
下面的 retainers 面板, 可以看到变量的具体引用路径, 在哪里被创建, 以及在哪里被使用
定位问题: 找到那些被引用本该被释放, 但实际没有的释放的对象
执行引起内存泄漏的操作
该操作的核心代码大致是这样的
主要功能是, 每次执行 setEvent, 都将 this.exception 指向新的实例, 并交给页面进行数据展示, 而之前被 this.exception 引用的对象, 应该被释放.
重新收集新的内存快照信息
找出差异: 将视图改为差异视图
从图上可以看到在步骤 1 之后, 出现了很多新增的对象, 但是删除的对象是 0.
以 Exception 对象为例, 按照步骤 1 的代码逻辑, 新对象建立, 旧对象被释放, Delta 应该为零. 所以可以明确知道, 这里是一个问题. 不过这里点开查看变量的引用详情, 并没得到太多有用的信息, 只知道被哪个 vue component 引用了, 但是 component 太多, 不太好定位.
查看 Listenters, 我看到的画风基本是这样的:
跟预期的结果一致, 都是由于一些 Nodes 没有被释放导致的. 不过确实没有得到太多方便分析的信息.
另外查看 Nodes 相关的信息, 搜索 Detached, 可以看到一些 Detatched htmlDivElement 等等类似的对象, 也就是在内存中但是没有在页面进行渲染的元素
我找了一个 detla 比较小的, 节点功能也清晰 (就是用来在页面中进行代码高亮的元素) 的 Detatched HTMLPreElement 进行分析:
可以看到实际引用关系为 div <= div <= div <= vue component <= var-hover <= events <= ... $platform.event...
在这里 $platform.event 是由平台 + 模块的架构设计中, 平台提供的事件 API, 用于全局的事件通信.
最终将以上引用关系进行翻译: 由平台提供的事件 $platform.event (全局, 绑定的事件函数不会被自动释放), 绑定了一个叫做 var-hover 的事件 => var-hover 的事件函数中引用了一个 vue-component => component 的 $el 属性 引用了某个 Dom => Dom 的父级被子级引用导致不能被 GC.
可以看看 var-hover 的代码:
var-hover 绑定了一个匿名函数(基本上也可以知道, 没有给这个事件没有写过解绑操作), 然后匿名函数中使用了 this, 也就是当前 vue component, 这也导致了被这个 component 引用的对象都不能被 GC.
所以祸根基本上找到了, 接下来要做的就是: 修复 -> 重新验证
修复
第一次简单修改: 在 beforeDestroy 中进行事件解绑, 当时验证确认内存溢出问题已解决
手动解绑这是个大坑, 很多地方很多人在编写代码的时候, 真不一定有这个好习惯. 所有也就有了现在的处理方案: 对平台接口进行改造, 支持事件的基于组件的自动解绑. 代码如下:
这就是 $platform.event 的实际实现
var-hover 的事件绑定如下
移除了 beforeDestroy 钩子, 业务层看起来也好多了.
验证
利用 performance 功能, 多次进行之前导致内存溢出的操作, 得到结果如下
这里的每次峰值, 就是刚执行进行操作时进行内存分配的结果, 之后每次执行, 并没有出现内存及 Nodes, Listensers 的累积
再次对比一下修正之前的性能分析结果
可怕的楼梯...
顺便再 memory 面板中出现了什么变化
多了一个 StackFrameVar 以及一些为了呈现这个 StackFrameVar 对象多出来的一些 EventListener,Observer 等, 这是由于两次呈现的数据本身不一样导致的, 属于正常情况
Exception, 等很多对象的 Delta 已经为 0 了(按 Delta 倒序排列的)
其它说明
以上分析图是写这篇文章过程中, 回写部分代码之后实时分析的, 相对而言没有实际调试时处理得那么细致. 实际调试过程中还做了其它操作:
隐私窗口, 禁用所有扩展(避免影响内存分析)
关闭开发模式 HMR 功能, 因为 VUE_HOT_RELOAD 也会产生一层引用, 我并不能完全信任它
使用模拟数据, 每次执行操作, 都会渲染一样的可被人工计算清楚 (知道哪个类会产生多少实例) 的数据
performance 过程中手动 GC
通过以上方式是为了提供一个完全纯净可控的分析环境.
来源: https://juejin.im/post/5c3dce07e51d4551e960d840