首页 > 系统相关 >使用内存堆快照检测分离的 DOM 内存泄漏

使用内存堆快照检测分离的 DOM 内存泄漏

时间:2024-11-06 13:42:13浏览次数:1  
标签:泄漏 快照 DOM Chrome GC 内存

https://fe.okki.com/post/62cbfea7136f570343d89416/

 

使用 Chrome Devtools 分析内存问题

网页的内存限制

对于 Chrome 浏览器,每个 Tab 页能使用的内存大小是有限制的。限制大小根据 Chrome 版本,Chrome位数(32/64),操作系统版本,会有所不同。可以通过 window.performance.memory 查看内存限制信息。

查看内存信息

对于在现代操作系统运行的较新版本 chrome,单tab的内存限制在 1.8G左右, 一旦超出内存限制,浏览器就会崩溃。正常情况下我们编写的代码使用不了这么多的内存,不过如果代码中存在内存泄漏问题,则有可能在较长时间操作后超出内存限制。

浏览器崩溃

何为内存泄漏

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

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

  • 闭包使用不当引起内存泄漏
  • 分离的 DOM 节点
  • 全局变量和全局绑定的事件
  • 遗忘的定时器

内存泄漏的问题一般不容易发现,好在我们可以使用 chrome 浏览器的开发者工具协助定位内存泄漏问题。

使用 Chrome Task Manager 观察实时 JS 内存使用

打开 Chrome 右上角设置-更多工具-任务管理器

chrome任务管理器

在 header 上右键将 JavaScript 使用的内存打勾,这里解释下 **内存 和 JavaScript 使用的内存 的区别。

  • 内存表示 native momory 占用,DOM 节点保存在 native momory。
  • JavaScript 使用的内存 表示 JS 堆内存,有括号外和括号内两个值。我们关注括号内的实际值,这个值代表了当前页面可访问(reachable)的对象占用了多少内存,直白一点就是实际被使用,不会被 GC 的对象占用的内存大小。

JS内存使用情况

使用 Performance 可视化检测内存泄漏

在 Chrome DevTools 的 Performance 面板勾选 Memory 并开启记录,之后在页面上执行一些操作。我们在记录数据的开始和结束时刻都手动进行一次 GC。

在结果图表中 HEAP 这一栏查看内存的使用情况,我们将一开始没有进行操作时的内存使用情况作为基准值,与最后停止操作并进行 GC 后的值进行比较,如果有较明显的差距,则说明产生了内存泄漏。

performance

在检测到发生了内存泄漏后,可以转到 Memory 面板,进行更进一步的分析。

memory

查看内存堆快照

在 Memory 面板选择 Heap snapshot 可以记录内存堆快照。内存堆快照中只包含可访问的对象,在开始记录内存堆快照前,Chrome 总是会进行一次 GC 操作。我们可以通过比较前后两次堆快照数据来分析内存泄漏问题。

使用内存堆快照检测分离的 DOM 内存泄漏

何为DOM内存泄漏

一个 DOM 节点只有在没有被引用时才会被 GC,当 DOM 节点不在 DOM 树中,但是却被 JS 代码保留着引用,这种情况下会造成内存泄漏。DOM 内存泄漏可能可能会比我们预想的严重,思考一下下面这个例子,tree 的内存在何时才能被 GC。

tree

var select = document.querySelector;
  var treeRef = select("#tree");
  var leafRef = select("#leaf");
  var body = select("body");
​
  body.removeChild(treeRef);
​
  //#tree can't be GC yet due to treeRef
  treeRef = null;
​
  //#tree can't be GC yet due to indirect
  //reference from leafRef
​
  leafRef = null;
  //#NOW can be #tree GC

答案是只有在执行到 leafRef = null 时才可以,虽然 leftRef 保留的只是 tree 的子节点 leafRef 的引用,但是因为leafRef 包含着对它 parmentNode 的引用,这个引用可以一直向上追溯到 tree,导致 GC 无法回收 tree节点。

检测 DOM 内存泄漏

可以输入 Detached 在结果中搜索分离状态的 DOM 节点。

下面这行代码创建了一个分离的 DOM 节点,在快照结果中搜索 Detached 可以找到这个分离的 DOM 节点,下方的信息告诉我们是 detached 这个变量保留了这个 DOM 节点的引用。

<script>
    const detached = document.createElement("div")
</script>

detached dom

查看内存分配时间轴信息

在 Memory 面板选择 Allocation instrumentation on timeline 可以记录内存分配时间轴信息。Chrome 会周期性以一定的间隔记录内存堆快照,并在记录结束时进行最后一次快照,以此生成内存分配时间轴信息。

开始记录后,在页面上进行一些操作,一段时间后停止记录,结果如下所示。

timeline

最上方的时间轴中,条形说明在该时间段发生了内存分配。条形的高度对应了分配的内存大小,条形的蓝色部分说明依然存活的对象数量,灰色部分说明已经被 GC 的对象数量。如果较早时间分配的对象依然大量存活,则说明可能有内存泄漏的问题。我们可以点击时间轴上该条形,检查该时间段内的详细内存分配信息。

实战 - 分析 OMS 项目内存使用情况,定位内存泄漏问题

整体思路:

现在测试环境检测内存使用趋势是否有问题,发现问题后开启本地开发环境具体分析问题。

准备工作:

打开 Chrome 的隐身模式,并关闭所有非必要的 Chrome 扩展,避免缓存和 Chrome 插件内存占用造成的影响,打开测试环境的采购入库单页面。

接下来定义一组模拟用户操作

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

这里使用了 Performance monitior 实时查看内存占用,在执行完一组用户操作后手动执行 GC 操作,观察内存增长情况。

performance monitor

发现每次操作完成后,内存稳定增加 12 M左右,接着采用控制变量的方式,发现不执行打开产品选择器的操作,内存几乎没有增加,推测是产品选择器出现了内存泄漏。

接着就具体找到哪里发生了泄漏,此时开启本地开发环境。在产品选择器中新建一个测试对象,观察该对象是否被正常 GC。

test object

在采购入库列表记录第一次快照,然后进入编辑页,打开产品选择器,再回到列表页,记录第二次快照,比较两次快照,在结果中搜索 TestObject。

analysis

发现果然没有被 GC 掉,同时路径中 popup-manager.js 这个文件很可疑,会不会是这里面缓存了组件实例呢?

尝试打印 popup-manager 的实例,发现 popups 依然保留着产品选择器的实例,看来是这个原因导致了内存泄漏。

console

知道了原因,接下来就是有的放矢,分析为啥产品选择器关闭时没有将其冲 popups 中移除,再修改代码,这里就比较简单了。定位到是 amumu 的 Popup 在销毁时,没有调用 PopupManager 的 unregister 方法将实例从 popups 中移除。修改后再测试,没有发现产品选择器内存泄漏的问题。

// amumu/src/utils/popup/popup-manager.js
const PopupManager = {
  zIndex: (Vue.prototype.$amumu || {}).zIndex || 1000,
  popups: {},
  get nextZIndex() {
    return this.zIndex++
  },
​
  register(instance) {
    const popupId = (instance.popupId = uniqueId("popup-id-"))
    if (popupId && instance) {
      this.popups[popupId] = {
        popupId,
        instance,
      }
    }
  },
  unregister(instance) {
    delete this.popups[instance.popupId]
  },
  ...
}
​
// amumu/src/utils/popup/index.js
{
...
  mounted() {
    PopupManager.register(this)
    if (this.localVisible) {
      PopupManager.open(this)
    }
  },
  // 原因就在这里 销毁时没有调用 unregister,popups 保留了vue实例的引用
  beforeDestroy() {
    PopupManager.close(this)
    this.removeScrollEffect()
  },
  ...
}

通过 git 的 commit 记录显示,这是一个四年前就存在的问题,在此之前一直没有被发现。而运用 Chrome 的开发者工具进行内存分析,我们轻易就发现并解决了这个问题。

标签:泄漏,快照,DOM,Chrome,GC,内存
From: https://www.cnblogs.com/chinasoft/p/18530011

相关文章

  • 借助 Microsoft Edge 分离的元素面板分析内存泄漏问题
    https://fe.okki.com/post/63721db5e81bf53bcf1cbff7/ MicrosoftEdge分离元素面板工具简介这是基于chromium内核的Edge浏览器,为开发者提供的一个用来分析网页DOM内存泄漏的工具,目前Chrome上还没有类似的工具。这个面板的界面如下,左上方四个按钮功能分别为获取分......
  • Js Dom
    DOM,全称DocumentObjectModel,中文翻译为文档对象模型。DOM属于WebAPI的一部分。WebAPI中定义了非常多的对象,通过这些对象可以完成对网页的各种操作(添加删除元素、发送请求、操作浏览器等)DOM中的D意为Document,即文档。所谓文档就是指整个网页,换言之,DOM是用来操作网页的。O......
  • 初学Java基础---Day21---正则表达式,日期类,Math类,Random类,System类,Runtime类,大数值运
    一,正则表达式理解:        符合某个语句规范的字符串案例://案例:把一个字符串中带电话号码替换成130****1111的形式Stringstr="小红13012341111小绿15112342222小黑13912343333";//分析:电话号码可以分为三组如:(130)(1234)(1111)其中第一组中的1是固定/......
  • 大数据新视界 -- 大数据大厂之 Impala 与内存管理:如何避免资源瓶颈(上)(5/30)
           ......
  • tracemalloc库追踪代码申请的内存大小
    为python代码{%code%}添加如下代码:importtracemalloctracemalloc.start()f=open('mem_size.txt','w',encoding='utf8'){%code%}snapshot=tracemalloc.take_snapshot()top_stats=snapshot.statistics('lineno')forstatintop_stat......
  • 2024/11/5日 日志 关于BOM浏览器对象模型和DOM文档对象模型的学习与笔记整理
    和Javascript有关的BOM与DOM及事件监听。以下是今天的内容点击查看代码--BOM--BrowserObjectModel浏览器对象模型--JavaScript将浏览器的各个组成部分封装为对象--组成:--Window:浏览器窗口对象--Navigator:浏览器对象--Screen:屏幕对象--History:历史记录......
  • 操作系统学习笔记-3.1内存管理
    文章目录内存的地址绝对装入静态重定位动态重定位链接覆盖和交换1.覆盖(Overwrite)在内存管理中的作用2.交换(Swap)在内存管理中的作用连续分配管理方式固定分区分配的关键概念优点缺点示例动态分区分配的关键概念优点缺点示例基本分页存储管理基本地址变换机构页表寄存......
  • Java内存区域详解(重点)
    运行时数据区域Java虚拟机在执行Java程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK1.8和之前的版本略有不同,我们这里以JDK1.7和JDK1.8这两个版本为例介绍。JDK1.7:JDK1.8: 线程私有的:程序计数器虚拟机栈本地方法栈线程共享的:堆方法区......
  • 享元模式及其运用场景:结合工厂模式和单例模式优化内存使用
    介绍享元模式(FlyweightPattern)是一种结构型设计模式,它通过共享对象来减少内存使用,尤其是对于大量相似对象的场景。享元模式通常与工厂模式和单例模式结合使用,从而有效地控制和复用对象的创建。在享元模式中,享元对象的核心思想是将不可变的部分(共享的状态)和可变的部分(外部......
  • 【Linux】进程间通信(命名管道、共享内存、消息队列、信号量)
                                 作者主页:   作者主页                           本篇博客专栏:Linux                ......