b2科目四模拟试题多少题驾考考爆了怎么补救
b2科目四模拟试题多少题 驾考考爆了怎么补救

哪些场景会造成内存泄漏哪些情况会引起的挂载泄漏

电脑杂谈  发布时间:2021-06-06 08:04:25  来源:网络整理

内存泄漏

问:什么是内存泄漏?

字面意思是请求的内存没有及时回收,被泄露了

问:为什么会出现内存泄漏?

虽然前端有垃圾回收机制,但是当一块无用的内存不能被垃圾回收机制认为是垃圾时,就会发生内存泄漏。

垃圾收集机制通常使用标志移除策略。简而言之,就是根据根节点是否可达来判断是否垃圾。

以上就是内存泄漏的根本原因。直接的原因是,当两个生命周期不同的东西相互通信时,一方生命到期了,就应该回收,但是当另一方还持有时,也会产生记忆。泄露

那么,我们来谈谈哪些场景会导致内存泄漏

哪些情况会导致内存泄漏1.意外的全局变量

全局变量的生命周期最长,会一直存活到页面关闭,所以全局变量上的内存永远不会被回收

当全局变量使用不当,没有及时回收(手动赋值为空值),或者拼写错误等,当一个变量挂载到全局变量时,就会发生内存泄漏。

2.被遗忘的计时器

setTimeout 和 setInterval 是浏览器维护其生命周期的专用线程,所以当在页面上使用定时器时,当页面被销毁时,如果不手动释放和清理这些定时器,那么这些定时器还活着

也就是说定时器的生命周期是不附加在页面上的,所以当在当前页面的js中通过定时器注册一个回调函数,并且回调函数持有当前页面的一个变量或者一些DOM元素,即使页面被销毁,因为定时器持有部分页面引用,页面无法正常回收,导致内存泄漏

如果这时候再打开同一个页面,内存中其实有双页数据。如果多次关闭和打开,内存泄漏会越来越严重

而且这种场景很容易出现,因为使用定时器的人很容易忘记清除它

3. 闭包使用不当

函数本身会保存对定义它的词法环境的引用,但通常在函数被使用后,函数请求的内存会被回收

但是当函数返回一个函数时,因为返回的函数持有外部函数的词法环境,而返回的函数又被其他生命周期的东西持有,所以执行了外部函数,但是内存不可用回收

所以,返回函数的生命周期不能太长,这样闭包才能及时回收

通常情况下,闭包不是内存泄漏,因为这个保存外部函数的词法环境是闭包的一个特性,就是防止这块内存被回收,因为以后可能会用到,但是这无疑会造成内存消耗,所以不要乱用

4. 缺少 DOM 元素

一个DOM元素的正常生命周期取决于它是否挂载在DOM树上。当从 DOM 树中移除时,它可以被销毁和回收。

但是如果一个 DOM 元素在 js 中也持有它的引用,那么它的生命周期是由 js 和它是否在 DOM 树上决定的。记得取下,这两个都需要清理干净才能正常回收

5.网络回调

在某些场景下,在页面上发起网络请求并注册一个回调,页面的一些内容保存在回调函数中。那么,当页面被销毁时,网络回调应该取消,否则,因为网络持有页面的部分内容,也会导致部分页面内容没有被回收

如何监控内存泄漏

内存泄漏可以分为两类。一种是比较严重的,泄露了就再也找不回来了。另一个稍微不那么严重,就是没有及时清理造成的内存泄漏。过一段时间还是可以清理的

无论是哪一种,开发者工具捕获的内存映射都应该在一段时间内看到内存使用量的连续线性下降。这是由于垃圾回收导致的​​GC不断发生。

对于第一个严重的,你会发现即使在内存图中GC继续发生后,使用的内存总量仍在增长

另外,内存不足会导致GC不断,GC会阻塞主线程,所以会影响页面性能,造成延迟,所以内存泄漏问题还是需要注意的

我们假设这样一个场景,然后使用开发者工具来检查内存泄漏:

场景一:在函数中申请一块内存,然后在短时间内连续调用该函数

// 点击按钮,就执行一次函数,申请一块内存
startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);
});

js内存泄漏是什么意思

一个页面可以使用的内存是有限的。当内存不足时,会触发垃圾回收机制回收无用的内存

函数内部使用的变量都是局部变量。函数执行后,这块内存没用了,可以回收

所以当我们在短时间内继续调用该函数时,可以发现在执行该函数时,发现内存不足,垃圾回收机制起作用,上一个函数请求的内存被回收因为前面的函数已经执行了,内存没用了 可以回收

所以图中显示内存使用情况的图表是过去的一条横线,中间有多条竖线。其实就是说内存是空的,再次申请,清空后再申请。每条竖线的位置是垃圾收集机制的工作和函数执行和应用的时机

场景二:在一个函数中申请一块内存,然后在短时间内不断调用该函数,但每次请求的一部分内存都被外部持有

// 点击按钮,就执行一次函数,申请一块内存
var arr = [];
startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);
    arr.push(b);
});

js内存泄漏是什么意思

看看和第一张图有什么不同?

不再是水平线,水平线中每条垂直线的底部不再是同一水平线。

其实这是内存泄漏。

我们在函数中申请了两个数组内存,但是其中一个数组是在外部保存的。那么,即使函数每次都被执行,这部分外部持有的数组内存仍然无法回收,所以每次只能回收一部分内存

这样,当函数调用次数增加时,无法回收的内存越多,内存泄漏就越多,从而导致内存使用量不断增长

另外,还可以使用性能监控工具,在开发者工具中找到更多的按钮,在里面打开这个功能面板。这是一款可以实时监控cpu、内存等使用情况的工具。工具爬一段时间比较直观:

js内存泄漏是什么意思

阶梯状的上升是存在内存泄漏。每次调用一个函数,总会有一部分数据被外界保留,无法恢复,而后面的顺利是每次使用都可以正常恢复

应该注意这张图片。在第一个红框的末尾有一个线性下降。这是因为我修改了代码,去掉了在外部保持函数中申请数组的那行代码,然后刷新页面,手动点击GC触发效果,否则不管怎么点击GC,都会有一些内存不会被回收,无法达到这样的效果图。

以上是监控是否发生内存泄漏的一些工具,但是下一步才是关键。既然发现了内存泄漏,那如何定位呢?怎么知道哪部分数据没有恢复?

如何分析内存泄漏并找出有问题的代码

分析内存泄漏的原因,还是需要用到开发者工具的Memory功能。该函数可以捕获内存快照,或者捕获一段时间内的内存分配,并触发一段时间内的内存分配。各功能

js内存泄漏是什么意思

使用这些工具,我们可以分析出某个时刻是哪个函数操作导致了内存分配,分析了大量重复的对象没有被回收

这样,可疑函数也知道了,可疑对象也知道了,然后去代码分析一下这个函数中的对象是否是内存泄漏的元凶,就搞定了

先举一个简单的例子,再举一个实际内存泄漏的例子:

场景一:在一个函数中申请一块内存,然后在短时间内连续调用该函数,但每次申请的内存都被外部占有了一部分

// 每次点击按钮,就有一部分内存无法回收,因为被外部 arr 持有了
var arr = [];
startBtn.addEventListener("click", function() {
	var a = new Array(100000).fill(1);
	var b = new Array(20000).fill(1);
    arr.push(b);
});

js内存泄漏是什么意思

可以抓取两个快照,在两个快照中间进行一次内存泄漏操作,最后比较两个快照之间的差异,看看添加的对象是什么,回收了哪些,如上图.

也可以单独查看某个时间的快照,从内存占用率看哪些对象占用了大量内存,如下图:

js内存泄漏是什么意思

从垃圾回收机制来看,可以查看GC根节点的哪些可达对象占用了大量内存:

js内存泄漏是什么意思

栈内存和堆内存题目js_js内存泄漏定义_js内存泄漏是什么意思

从上面的方法开始,可以看到当前占用大量内存的是什么。一般来说,这就是嫌疑人。

当然不一定。当存在可疑对象时,可以使用多个内存快照进行比较,并在中间手动强制GC查看是否恢复了恢复的对象。这是一种思维方式

js内存泄漏是什么意思

js内存泄漏是什么意思

这样就可以有选择地查看每个内存分配时刻启动了哪个函数,内存中存放的是什么对象

当然,内存分配是正常行为,需要通过其他数据来判断一个对象是否为可疑对象,比如内存使用比例,或者结合内存快照等。

js内存泄漏是什么意思

能看到的内容很少,比较简单,目的也很明确,就是在一段时间内申请内存有哪些操作,使用了多少

总之,这些工具没有办法直接回答你,告诉你xxx是内存泄漏的罪魁祸首。如果浏览器级别可以确定,那为什么不回收,为什么会造成内存泄漏?

所以,这些工具只能给你各种内存使用信息,你需要自己使用这些信息,根据自己代码的逻辑,分析出哪些嫌疑人是内存泄漏的元凶

案例分析

网上很多文章都出现过的内存泄漏例子:

var t = null;
var replaceThing = function() {
  var o = t
  var unused = function() {
    if (o) {
      console.log("hi")
    }        
  }
 
  t = {
        longStr: new Array(100000).fill('*'),
        someMethod: function() {
                       console.log(1)
                    }
      }
}
setInterval(replaceThing, 1000)

可能你还没看到这段代码会不会导致内存泄漏,是什么原因,别着急

先说一下这段代码的用途,声明一个全局变量t和replaceThing函数,函数的目的是给全局变量赋值一个新的对象,然后里面有一个变量来存储的值替换之前的全局变量t,最后是定时器周期性地执行replaceThing函数

先用工具看看会不会有内存泄漏:

js内存泄漏是什么意思

三个内存监控图都显示发生了内存泄漏:重复执行同一个函数,但内存逐步增长,手动点击GC内存,内存没有减少,说明有部分内存每次执行函数时都会泄漏。

这种手动强制垃圾回收不能使内存down的很严重。如果长时间执行,会耗尽可用内存,导致页面死机甚至崩溃

既然已经确定存在内存泄漏,那么是时候找出内存泄漏的原因了。

js内存泄漏是什么意思

首先,通过采样配置文件,我们在replaceThing函数上定位嫌疑人

接下来,我们抓取两张内存快照并进行比较,看看是否可以得到任何信息:

js内存泄漏是什么意思

对比两张快照,我们可以发现在这个过程中数组对象一直在增加,而这个数组对象来自于replaceThing函数内部创建的对象的longStr属性

其实这张图有很多信息,尤其是下面嵌套的图。嵌套关系是相反的。往后看,可以从全局对象Window中一步步找到数组对象的访问方式,没错,垃圾回收机制就是因为这样一个可达的访问路径无法回收。

其实你可以在这里分析一下。为了使用更多的工具,我们换个图来分析一下。

我们直接从第二个内存快照开始看:

js内存泄漏是什么意思

从第一个快照到第二个快照,replaceThing 执行了 7 次,只创建了 7 个对象。似乎这些对象还没有被回收

那为什么不回收利用呢?

replaceThing 函数只是在内部保存了之前的对象,但是函数执行结束,局部变量不应该被回收吗?

继续看图,可以看到下面有个闭包,占用了很多内存,见:

js内存泄漏定义_栈内存和堆内存题目js_js内存泄漏是什么意思

js内存泄漏是什么意思

为什么每次调用replaceThing函数都不能回收内部创建的对象?

因为是第一次创建replaceThing,这个对象被全局变量t持有,所以不能回收

对于后续的每次调用,这个对象都由之前的replaceThing函数内部的o局部变量持有,不能回收

该函数中的局部变量o由第一次调用replaceThing时创建的对象的someMethod方法持有,该方法挂载的对象由全局变量t持有,因此无法回收

像这样层层持有,每次调用函数时,都会持有上次调用该函数时内部创建的局部变量,所以即使函数被执行,这些局部变量也无法回收

说是盗图(侵权删除)有点傻,结合垃圾收集机制的标记去除方法(俗称可达性方法),就很清楚了:

盗自 https://juejin.im/post/5979b5755188253df1067397

根据使用内存分析工具,可以得到如下信息:

同一个函数调用的内存使用量呈阶梯状增加,手动GC内存无法减少,说明内存泄漏,捕获了一段时间的内存申请情况。可以确定可疑函数为replaceThing,找到内存快照。没有回收的是replaceThing里面创建的对象(包括存储数组的longStr属性和方法someMethod)。进一步分析内存快照发现不回收的原因是因为每次函数调用创建的对象都存放在上次调用内部创建的局部变量o是on,在函数末尾没有回收局部变量o执行,因为它被创建的对象的 someMethod 方法持有

以上是结论,但我们还是要分析一下为什么会这样,对

其实这涉及到闭包的知识:

MDN 对闭包的解释是,将功能块和定义该函数的词法环境组合起来称为闭包

定义函数时,有作用域的内部属性存储当前词法环境。因此,一旦一个函数被生命周期比它所在词法环境更长的东西所持有,这将导致该函数所持有的词法环境无法被回收

简单来说,当外部持有一个函数中定义的函数时,此时如果内部函数使用了外部函数的一些变量,那么即使外部函数执行结束,这些变量也无法回收,因为相反,它存储在内部函数的属性中

还有一个知识点。外部函数中定义的所有函数共享一个闭包,即函数b使用了外部函数a的变量。即使不使用 c 函数,c 函数仍会存储 a 变量。这叫做共享闭包

回到这个问题

因为replaceThing函数手动将内部创建的literal对象赋值给全局变量,而且这个对象也有someMethod方法,所以someMethod方法存储replaceThing的变量因为闭包特性

虽然someMethod没有使用任何局部变量,但是replaceThing里面有一个未使用的函数。该函数使用局部变量 o。由于共享闭包,someMethod还存储了o

而o在替换前保存了全局变量t的值,所以导致每次调用函数时,内部变量o都会被人持有,无法回收

要解决这个内存泄漏,需要切断o的持有者,这样局部变量o才能正常回收

所以有两个想法:要么让 someMethod 不存储 o;或使用后释放o;

如果没用的函数没用,可以去掉这个函数看看效果:

js内存泄漏是什么意思

这里之所以会出现阶梯上升,是因为当前内存还够用,没有触发垃圾回收机制。可以手动触发GC,也可以运行一段时间等待GC开始工作,查看内存是否下降到初始阶段。状态,表示这些记忆是可以回收的

或者拉一张内存快照看看。拉取快照时,会自动先强制GC再拉取快照:

js内存泄漏是什么意思

是的,即使周期性调用replaceThing函数,即使函数中的局部变量o存储了最后一个全局变量t的值,毕竟是局部变量。函数执行完后,如果没有外部引用它,它会被回收,所以最终的内存只是全局变量t存储的对象

当然,如果没用的函数不能去掉,那只能记得用完后手动释放o变量:

var unused = function() {
    if (o) {
      console.log("hi")
      o = null;
    }        
}

但是这种方式并不能根治,因为在未使用的函数执行之前,内存堆仍然存在,或者已经泄漏无法回收。和一开始的区别是,至少在未使用的函数执行完之后,你可以释放它

其实这里应该考虑的代码有没有问题,为什么需要局部变量存储,为什么需要存在一个未使用的函数,这个函数的用途是什么,如果只是使用的话判断未来某个时刻的最后一个全局变量是否可用,那么为什么不直接使用一个全局变量来存储它,为什么要选择一个局部变量呢?

所以,在写代码的时候,涉及到闭包场景的时候,要特别注意。如果使用不当,很可能会造成一些严重的内存泄漏场景

需要记住的是,闭包会允许函数持有一个外部词法环境,导致外部词法环境中的一些变量无法被回收。还有一个共享闭包的特性。只有知道了这两点,才能参与到闭包场景的使用中,合理考虑如何实现,避免出现严重的内存泄漏


本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-380806-1.html

    相关阅读
      发表评论  请自觉遵守互联网相关的政策法规,严禁发布、暴力、反动的言论

      热点图片
      拼命载入中...