什么?!你居然连个内存泄漏都排查不出来

公众号:程序员白特,欢迎一起交流学习~

在日常的业务开发中,偶尔会出现内存泄漏的情况,那么我们该怎么排查呢?现在跟着文章一起学习下吧~

使用Chrome devTools查看内存情况

打开Chrome的无痕模式,以屏蔽Chrome插件对我们之后测试内存占用情况的影响。然后打开开发者工具,找到Performance栏,可以看到一些功能按钮,如开始录制按钮、刷新页面按钮、清空记录按钮、记录并可视化js内存、节点、事件监听器按钮、触发垃圾回收机制按钮等。

请简单录制一下百度页面,观察我们能够获取到什么信息,如下动图所示:

从图表中我们可以清楚地观察到,在页面加载过程中JS Heap(js堆内存)、documents(文档)、Nodes(DOM节点)、Listeners(监听器)、GPU memoryGPU内存)的最低值、最高值以及随时间的变化趋势,这是我们关注的重点。

查看开发者工具中的Memory一栏,主要用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况。

堆快照类似于照相机,可以记录当前页面的堆内存情况。每次进行快照,都会生成一条快照记录。

根据上图所示,我们首先进行了一次快照,记录了当时堆内存空间占用为33.7MB。随后,我们点击了页面中的一些按钮,再次执行了一次快照,记录了当时堆内存空间占用为32.5MB。此外,通过点击相应的快照记录,我们可以查看当时所有内存中的变量情况,包括结构和占总内存的百分比等信息。

在记录数据后,我们可以观察到图表右上角有起伏的蓝色和灰色柱状图,其中蓝色代表当前时间线下所占用的内存;灰色表示之前占用的内存空间已被清除释放。

在发现存在内存泄漏的情况时,我们可以使用Memory来更清晰地确认问题并定位问题。

首先,可以使用Allocation instrumentation on timeline来确认问题,如下图所示:.

内存泄漏的场景

  • 闭包使用不当引起内存泄漏
  • 全局变量
  • 分离的DOM节点
  • 控制台的打印
  • 遗忘的定时器

1. 闭包使用不当引起内存泄漏

使用PerformanceMemory来查看一下闭包导致的内存泄漏问题

<button onclick="myClick()">执行fn1函数</button>
<script>function fn1 () {let a = new Array(10000)  // 这里设置了一个很大的数组对象let b = 3function fn2() {let c = [1, 2, 3]}fn2()return a}let res = [] function myClick() {res.push(fn1())}}
</script>

fn1函数执行上下文退出后,本应将该上下文中的变量a视为垃圾数据并进行回收。然而,由于fn1函数最终将变量a返回并赋值给全局变量res,这导致对变量a的引用产生,使得变量a被标记为活动变量并一直占用相应的内存。如果假设后续不再使用变量res,那么这就是一个闭包使用不当的例子。

为了能够在performance的曲线图中观察效果,我们设置了一个按钮,每次点击执行时,将fn1函数的返回值添加到全局数组变量res中。如下图所示:

  • 在每次录制开始时手动触发一次垃圾回收机制,这是为了确认一个初始的堆内存基准线,便于后面的对比。然后我们点击了几次按钮,即往全局数组变量res中添加了几个比较大的数组对象。最后再触发一次垃圾回收,发现录制结果的JS Heap曲线刚开始成阶梯式上升的,最后的曲线的高度比基准线要高,说明可能是存在内存泄漏的问题。

  • 在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确地确认问题和定位问题。

  • 首先可以使用Allocation instrumentation on timeline来确认问题,如下图所示:.

  • 每次点击按钮后,动态内存分配情况图上都会出现一个蓝色的柱形,而且在我们触发垃圾回收后,蓝色柱形都没有变成灰色柱形,也就是说之前分配的内存没有被清除。

  • 因此,我们可以明确地确认存在内存泄漏的问题。接下来,我们需要精确定位问题,可以使用Heap snapshot来进行定位,如下图所示:

  • 首先,我们需要点击快照记录初始的内存情况,然后多次点击按钮后再次点击快照,记录此时的内存情况。我们发现,从原来的1.1M内存空间变成了1.4M内存空间。接着,我们选中第二条快照记录,可以看到右上角有一个All objects的字段,表示展示当前选中的快照记录所有对象的分配情况。我们想要知道的是第二条快照与第一条快照的区别在哪,因此选择Object allocated between Snapshot1 and Snapshot2,即展示第一条快照和第二条快照存在差异的内存对象分配情况。这时,我们可以看到Array的百分比很高,初步可以判断是该变量存在问题。点击查看详情后,就能查看到该变量对应的具体数据了。

这是一个判断闭包是否导致内存泄漏问题并简单定位的方法

2. 全局变量

全局变量通常不会被垃圾回收,但并非所有变量都不能存在于全局范围。有时由于疏忽,会导致某些变量流失到全局,比如未声明变量,却直接对其赋值,这将导致该变量在全局范围创建。如下所示:

function fn1() {// 此处变量name未被声明name = new Array(99999999)
}
fn1()
  • 此时,当出现这种情况时,会自动在全局范围内创建一个变量name,并将一个大型数组赋值给name。由于它是全局变量,所以该内存空间将一直保持不释放。

  • 要解决这个问题,我们需要自己在平时多加注意,不要在变量声明之前进行赋值。另外,也可以考虑开启严格模式,这样在不知不觉中犯错时,会收到错误警告。例如,

function fn1() {'use strict';name = new Array(99999999)
}
fn1()

3. 分离的DOM节点

如果您手动删除了一个dom节点,本应该释放该节点占用的内存,但由于疏忽导致某处代码仍然引用了该被移除节点,最终导致该节点占用的内存无法被释放,这种情况是很常见的。

<div id="root"><div class="child">我是子元素</div><button>移除</button>
</div>
<script>let btn = document.querySelector('button')let child = document.querySelector('.child')let root = document.querySelector('#root')btn.addEventListener('click', function() {root.removeChild(child)})
</script>

代码的功能是在点击按钮后移除.child节点,尽管节点在点击后确实从dom中移除了,但全局变量child仍然保留对该节点的引用,导致该节点的内存无法释放,建议使用Memory的快照功能进行检测,具体操作如下图所示。

先记录下初始状态的快照,然后在点击移除按钮后,再次点击一次快照。此时,我们无法看出内存大小的任何变化,因为被移除的节点占用的内存非常小,可以忽略不计。但是,我们可以点击第二条快照记录,在筛选框中输入“detached”,这样就会显示所有脱离但尚未被清除的节点对象。

解决办法如下图所示:

<div id="root"><div class="child">我是子元素</div><button>移除</button>
</div>
<script>let btn = document.querySelector('button')btn.addEventListener('click', function() { let child = document.querySelector('.child')let root = document.querySelector('#root')root.removeChild(child)})
</script>

修改非常简单,只需将对.child节点的引用移动到click事件的回调函数中。这样,当移除节点并退出回调函数的执行上下文后,对该节点的引用将自动清除,从而避免了内存泄漏的情况。让我们来验证一下。如下图所示:

结果很明显,这样处理过后就不存在内存泄漏的情况了

4. 控制台的打印

<button>按钮</button>
<script>document.querySelector('button').addEventListener('click', function() {let obj = new Array(1000000)console.log(obj);})
</script>

我们在按钮的点击回调事件中创建了一个很大的数组对象并打印,用performance来验证一下

开始录制时,首先进行一次垃圾回收以清除初始内存。然后点击按钮三次,即执行了三次点击事件。最后再次触发一次垃圾回收。观察录制结果发现,JS Heap曲线呈阶梯状上升,并且最终保持的高度比初始基准线高很多。这说明每次执行点击事件时,创建的大型数组对象obj由于被浏览器保存并且无法回收,导致内存占用增加。

接下来注释掉console.log,再来看一下结果:

<button>按钮</button>
<script>document.querySelector('button').addEventListener('click', function() {let obj = new Array(1000000)// console.log(obj);})
</script>

可以看到没有打印以后,每次创建的obj都立马被销毁了,并且最终触发垃圾回收机制后跟初始的基准线同样高,说明已经不存在内存泄漏的现象了

其实同理 console.log也可以用Memory来进一步验证

未注释 console.log

注释掉了console.log

最后简单总结一下:在开发环境下,可以使用控制台打印便于调试,但是在生产环境下,尽可能得不要在控制台打印数据。所以我们经常会在代码中看到类似如下的操作:

// 如果在开发环境下,打印变量obj
if(isDev) {console.log(obj)
}

这样就避免了生产环境下无用的变量打印占用一定的内存空间,同样的除了console.log之外,console.errorconsole.infoconsole.dir等等都不要在生产环境下使用

5. 遗忘的定时器

定时器也是平时很多人会忽略的一个问题,比如定义了定时器后就再也不去考虑清除定时器了,这样其实也会造成一定的内存泄漏。来看一个代码示例:

<button>开启定时器</button>
<script>function fn1() {let largeObj = new Array(100000)setInterval(() => {let myObj = largeObj}, 1000)}document.querySelector('button').addEventListener('click', function() {fn1()})
</script>

这段代码是在点击按钮后执行fn1函数,fn1函数内创建了一个很大的数组对象largeObj,同时创建了一个setInterval定时器,定时器的回调函数只是简单的引用了一下变量largeObj,我们来看看其整体的内存分配情况吧:

按道理来说点击按钮执行fn1函数后会退出该函数的执行上下文,紧跟着函数体内的局部变量应该被清除,但图中performance的录制结果显示似乎是存在内存泄漏问题的,即最终曲线高度比基准线高度要高,那么再用Memory来确认一次:

  • 在我们点击按钮后,从动态内存分配的图上可以看到一个蓝色柱形,表示浏览器为变量largeObj分配了一段内存。然而,这段内存并没有被释放,这说明存在内存泄漏的问题。其实,问题的原因是setInterval的回调函数内对变量largeObj有一个引用关系,而定时器一直未被清除,所以变量largeObj的内存也自然不会被释放。

  • 那么,我们如何解决这个问题呢?假设我们只需要让定时器执行三次,我们可以对代码进行一些修改:

<button>开启定时器</button>
<script>function fn1() {let largeObj = new Array(100000)let index = 0let timer = setInterval(() => {if(index === 3) clearInterval(timer);let myObj = largeObjindex ++}, 1000)}document.querySelector('button').addEventListener('click', function() {fn1()})
</script>

现在我们再通过performancememory来看看还不会存在内存泄漏的问题

  • performance

这次的录制结果表明,最终曲线的高度与初始基准线的高度相同,这意味着没有发生内存泄漏。

  • memory

这里需要澄清一下,图中最初出现的蓝色柱形是因为我在录制后刷新了页面,可以忽略;接着我们点击了按钮,看到又出现了一个蓝色柱形,这时是为fn1函数中的变量largeObj分配了内存,3s后该内存被释放,变成了灰色柱形。因此可以得出结论,这段代码没有内存泄漏问题。

简要总结一下:在使用定时器时,务必在不需要定时器时清除,否则可能出现类似本例的情况。除了setTimeoutsetInterval,浏览器还提供了API,如requestAnimationFrame,也可能存在这种问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/726683.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

k个一组反转链表

题目 题目链接 . - 力扣&#xff08;LeetCode&#xff09; 题目描述 代码实现 class Solution { public:ListNode* reverseKGroup(ListNode* head, int k) {if(k 1) return head;//特殊情况ListNode *cur head;for(int i 1; i < k; i){if(cur nullptr ||cur->nex…

Seurat 中的数据可视化方法

本文[1]将使用从 2,700 PBMC 教程计算的 Seurat 对象来演示 Seurat 中的可视化技术。您可以从 SeuratData[2] 下载此数据集。 SeuratData::InstallData("pbmc3k")library(Seurat)library(SeuratData)library(ggplot2)library(patchwork)pbmc3k.final <- LoadData(…

【wine】解决 0024:fixme:msctf:KeystrokeMgr_TestKeyUp STUB:(00A3D508)

故障日志 0024:fixme:msctf:KeystrokeMgr_TestKeyUp STUB:(00A3D508) AI分析 这些消息表示Wine对IE内核组件以及IME&#xff08;Input Method Editor&#xff0c;输入法编辑器&#xff09;的支持不完全。特别是涉及文本输入、拖放事件、属性变化通知等功能。 解决 winetrick…

【论文阅读】单词级文本攻击TAAD2.2

TAAD2.2论文概览 0.前言1-101.Bridge the Gap Between CV and NLP! A Gradient-based Textual Adversarial Attack Frameworka. 背景b. 方法c. 结果d. 论文及代码 2.TextHacker: Learning based Hybrid Local Search Algorithm for Text Hard-label Adversarial Attacka. 背景b…

python爬虫(一)

一、python中的NumPy模块&#xff08;数据的存储和处理&#xff09; 这里是下载完成之后的表现 &#xff08;1&#xff09;创建数组 1、使用array&#xff08;&#xff09;函数创建数组 使用array函数可以创建任意维度的的数组 下面是一个创建二维数组的代码示例 下面是代码…

java集合(泛型数据结构)

1.泛型 1.1泛型概述 泛型的介绍 泛型是JDK5中引入的特性&#xff0c;它提供了编译时类型安全检测机制 泛型的好处 把运行时期的问题提前到了编译期间 避免了强制类型转换 泛型的定义格式 <类型>: 指定一种类型的格式.尖括号里面可以任意书写,一般只写一个字母.例如: …

【力扣 - 三数之和】

题目描述 给你一个整数数组 nums &#xff0c;判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k &#xff0c;同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。 注意&#xff1a;答案中不可以包含重复的三元组。…

PostgreSQL开发与实战(6.2)体系结构2

作者&#xff1a;太阳 二、逻辑架构 graph TD A[database] -->B(schema) B -->C[表] B -->D[视图] B -->E[触发器] C -->F[索引] tablespace 三、内存结构 Postgres内存结构主要分为 共享内存 与 本地内存 两部分。共享内存为所有的 background process提供内…

excel中去除公式,仅保留值

1.单个单元格去除公式 双击单元格&#xff0c;按F9. 2.批量去除公式 选中列然后复制&#xff0c;选择性粘贴&#xff0c;选值粘贴

windows server 2019 激活系统时点击“更改产品密钥”无反应的解决方案

一、问题现象 点击“更改产品密钥”没反应。 二、解决方案 使用slmgr命令&#xff1a; 打开命令提示符&#xff08;管理员&#xff09;&#xff0c;然后尝试使用slmgr命令来手动输入密钥和激活Windows。例如&#xff1a; slmgr.vbs /ipk <您的产品密钥>slmgr.vbs /ato 备…

软件测试技术分享 | 测试环境搭建

被测系统的环境搭建&#xff0c;是我们作为软件测试人员需要掌握的技能。 被测系统AUT (Application Under Test) 常见的被测系统即需要被测试的 app&#xff0c;网页和后端服务。大致分为两个方面移动端测试和服务端测试&#xff0c;如下图所示&#xff1a; 常见的被测系统类…

3、Redis Cluster集群运维与核心原理剖析

Redis集群方案比较 哨兵模式 在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态&#xff0c;如果master节点异常&#xff0c;则会做主从切换&#xff0c;将某一台slave作为master&#xff0c;哨兵的配置略微复杂&#xff0c;并且性能和高可用性…

【C语言】冒泡排序

概念 冒泡排序&#xff08;Bubble Sort&#xff09;是一种简单的排序算法&#xff0c;它重复地遍历要排序的列表&#xff0c;一次比较两个元素&#xff0c;并且如果它们的顺序错误就把它们交换过来。通过多次的遍历和比较&#xff0c;最大&#xff08;或最小&#xff09;的元素…

数智化转型的新篇章:企业如何在「数据飞轮」理念中寻求增长?_光点科技

在当今的数字化浪潮中&#xff0c;企业对数据的渴求与日俱增。数据不再仅是辅助决策的工具&#xff0c;而是成为推动业务增长的核心动力。自从「数据中台」概念降温后&#xff0c;企业纷纷探寻新的数智化路径。在这个过程中&#xff0c;「数据飞轮」作为一种新兴的理念&#xf…

Blazor系统教程(.net8)

Blazor系统教程 1.认识 Blazor 简单来讲&#xff0c;Blazor旨在使用C#来替代JavaScript的Web应用程序的UI框架。其主要优势有&#xff1a; 使用C#编写代码&#xff0c;这可提高应用开发和维护的效率利用现有的NET库生态系统受益于NET的性能、可靠性和安全性与新式托管平台(如…

第三方软件测试报告有效期是多久?专业软件测试报告获取

第三方软件测试报告是在软件开发过程中&#xff0c;由独立的第三方机构对软件进行全面测试和评估后发布的报告。这些第三方机构通常是与软件开发商和用户无关的专业技术机构&#xff0c;具备丰富的测试经验和专业知识。    第三方测试报告具有以下几个好处&#xff1a;   …

阿里云Linux系统MySQL8忘记密码修改密码

相关版本 操作系统&#xff1a;Alibaba Cloud Linux 3.2104 LTS 64位MySQL&#xff1a;mysql Ver 8.0.34 for Linux on x86_64 (Source distribution) MySQL版本可通过下方命令查询 mysql --version一、修改my.cnf文件 文件位置&#xff1a;etc/my.cnf进入远程连接后可以打…

落地灯哪个牌子好?实机测评喜爱度爆表的五款落地灯!

近些年来&#xff0c;由于使用电子产品以及学习压力大的人越来越多&#xff0c;而且越加年轻化&#xff0c;而平时用眼时的不良光线影响着人们的视力健康&#xff0c;不少眼科专家都推荐使用能够带来更好光线效果的落地灯&#xff0c;对此&#xff0c;作为专业的电器测评员&…

Pygame教程05:帧动画原理+边界值检测,让小球来回上下运动

------------★Pygame系列教程★------------ Pygame教程01&#xff1a;初识pygame游戏模块 Pygame教程02&#xff1a;图片的加载缩放旋转显示操作 Pygame教程03&#xff1a;文本显示字体加载transform方法 Pygame教程04&#xff1a;draw方法绘制矩形、多边形、圆、椭圆、弧…

Context

在 Android 开发中&#xff0c;Context 是一个表示应用程序环境的类&#xff0c;它提供了访问应用程序资源和执行应用程序级操作的接口。它是一个抽象类&#xff0c;具体的实现类是 ContextImpl。 Context 类的实例在整个 Android 应用程序中广泛使用&#xff0c;它可以用于执…