记一次网页内存溢出分析及解决实践

栏目: 编程语言 · 发布时间: 5年前

内容简介:项目是利用vue框架开发的公司内部的异常监控系统,用于显示java程序运行时的异常信息,包括执行堆栈、代码、变量等信息显示。在测试过程中,部门同事反映:在不同的异常信息之间多次切换,会导致网页崩溃。在案发现场打开 chrome 的任务管理器,看到这个页面内存占用已经达到了9.7G,初步怀疑页面存在泄漏。可以看到Nodes、Listeners、JS Head(memory) 的阶梯式增长,中间的增长节点对应就是每一次操作。很显然这个操作会引起内存的持续增长,最终发生内存溢出也是顺理成章了。

项目是利用vue框架开发的公司内部的异常监控系统,用于显示 java 程序运行时的异常信息,包括执行堆栈、代码、变量等信息显示。

在测试过程中,部门同事反映:在不同的异常信息之间多次切换,会导致网页崩溃。在案发现场打开 chrome 的任务管理器,看到这个页面内存占用已经达到了9.7G,初步怀疑页面存在泄漏。

验证猜测

  1. 打开 devtool -> performance,开始记录页面性能
  2. 执行页面上切换其它异常信息的操作(页面最有可能引起内存泄漏的操作)
  3. 查看性能分析结果
记一次网页内存溢出分析及解决实践

可以看到Nodes、Listeners、JS Head(memory) 的阶梯式增长,中间的增长节点对应就是每一次操作。很显然这个操作会引起内存的持续增长,最终发生内存溢出也是顺理成章了。

问题分析

在动手之前,我已知的信息有:

  1. 从performace工具,可以看到 JS Heap、Nodes、Listeners 的累积增长
  2. 以上三点,实际上存在依赖关系:
  • 变量引用DOM
  • 子级DOM不能释放,会导致父级也不会被释放
  • 如果DOM能被正常GC, 对DOM的事件监听器也会自动移除
  1. 可以使用 devtool->memory -> take snapshot 收集内存快照并分析 工具 文档;

简单认识一下 snapshot

记一次网页内存溢出分析及解决实践

嗯,内容有点多,但是也还算清晰:

  1. 按数据类型进行统计,可以看到一些内建对象、 Vue 对象、自定义对象(比如 Exception、StackFrame 等)、Detached Element、EventListener等等。
  2. 纵坐标有Distance, shallow size, Retained Size, 可以不准确理解为:
  • Distance:到root的引用距离
  • Shallow size:对象本身的大小,不包含它引用的数据的大小
  • Retained size:对象自身以及所有引用的大小,就是对象总共占用的内存
  1. 下面的 retainers 面板,可以看到变量的具体引用路径、在哪里被创建、以及在哪里被使用
记一次网页内存溢出分析及解决实践

定位问题:找到那些被引用本该被释放,但实际没有的释放的对象

  1. 执行引起内存泄漏的操作

该操作的核心代码大致是这样的

记一次网页内存溢出分析及解决实践

主要功能是,每次执行setEvent,都将 this.exception 指向新的实例,并交给页面进行数据展示,而之前被this.exception引用的对象,应该被释放。

  1. 重新收集新的内存快照信息

  2. 找出差异:将视图改为差异视图

记一次网页内存溢出分析及解决实践
从图上可以看到在步骤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。

所以祸根基本上找到了,接下来要做的就是:修复 -> 重新验证

修复

  1. 第一次简单修改:在 beforeDestroy 中进行事件解绑,当时验证确认内存溢出问题已解决
  2. 手动解绑这是个大坑,很多地方很多人在编写代码的时候,真不一定有这个好习惯。所有也就有了现在的处理方案:对平台接口进行改造,支持事件的基于组件的自动解绑。代码如下:
记一次网页内存溢出分析及解决实践
这就是$platform.event 的实际实现

var-hover的事件绑定如下

记一次网页内存溢出分析及解决实践
移除了 beforeDestroy 钩子,业务层看起来也好多了。

验证

  1. 利用 performance 功能,多次进行之前导致内存溢出的操作,得到结果如下
记一次网页内存溢出分析及解决实践
这里的每次峰值,就是刚执行进行操作时进行内存分配的结果,之后每次执行,并没有出现内存及 Nodes, Listensers 的累积

再次对比一下修正之前的性能分析结果

记一次网页内存溢出分析及解决实践
可怕的楼梯。。。
  1. 顺便再 memory 面板中出现了什么变化
记一次网页内存溢出分析及解决实践
多了一个 StackFrameVar 以及一些为了呈现这个 StackFrameVar 对象多出来的一些EventListener、Observer等,这是由于两次呈现的数据本身不一样导致的,属于正常情况

Exception、 等很多对象的 Delta 已经为0了(按 Delta倒序排列的)


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

零基础学Java Web开发

零基础学Java Web开发

刘聪 编 / 机械工业出版社 / 2008-1 / 59.00元

《零基础学Java Web开发:JSP+Servlet+Sfruts+Spring+Hibernte》全面讲解Java Web应用开发的编程技术,并详细介绍Java Web开发中各种常用的技术,可作为Java Web开发技术的学习指南。 《零基础学Java Web开发:JSP+Servlet+Sfruts+Spring+Hibernte》共17章,分为3篇,其中第1~12章是基础篇,讲解了......一起来看看 《零基础学Java Web开发》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具