前端内存泄露问题
最近发现项目有个bug,同时运行多个任务的时候,前端页面报内存不足而导致页面崩溃,这很明显就是内存泄露了。我查看了一下,运行的过程中,因为运行时间很久,所以前端和后台约定了,用计时器setInternal定时去请求后台运行状态,当运行状态为完成时,前端会清除定时器。我预估是因为计时器而导致的内存泄露,在执行计时器代码的时候,任务管理器的物理内存消耗一直在增加,这样的话,要是多个任务同时在执行,而且任务执行较久的话,那样物理内存就有可能会被占用完。后面我也复现了这个场景,果然是因为计时器的原因。
一、探究过程
疑惑1:会不会是因为JavaScript引擎是单线程,计时器会不断把事件不断放入事件队列,而任务执行时间很长,所以才会导致事件队列堆满而导致内存泄露?
资料答疑:首先,纠正大家一个错误的理解,定时器并不是严格意义上会按个多少秒执行的,它可能会出现执行延迟或提前。在运行本行代码的时候,将定时器的代码添加到了事件队列当中,而不是何时执行/运行代码。此时需要等到当前“事件处理程序”运行之后再去执行定时器代码。换句话说,就是,并非是设置的毫秒数后就执行定时器代码,执行的时间是有可能提前/延后的。同时,JavaScript引擎设置了:仅当队列中没有该定时器的任何其他代码实例时,才能够将定时器代码添加到队列。所以不会出现导致连续运行多次的情况。
疑惑2:那到底是什么导致内存泄露呢?
资料答疑:内联书写setInterval时,由于匿名函数被定义于全局中,不能够计时器的清除,因此很容易造成内存泄露。
二、实验测试
刚好我的计时器setInternal是用匿名函数写的,很有可能是因为这个原因,所以我用了匿名函数和命名函数测试了一下。
1、匿名函数
代码:
1 mounted(){
2 let self = this;
3 self.setInternalId = setInterval(()=>{
4 let sum = 0,i=1;
5 while(i<100000000){
6 sum+=i++;
7 // sum=parseInt(sum/2)
8 }
9 let now = new Date();
10 console.log(sum,'秒数:',now.getSeconds());
11 },2000);
12 },
13 beforeDestroy(){
14 let self = this
15 clearInterval(self.setInternalId);
16 console.log('消除定时器啦。。。。')
17 },
我发现物理内存是很缓慢增长的,所以要时间够长才能会有明显的区别,所以不能确定是匿名函数导致的。
2、命名函数
代码:
1 mounted(){
2 let self = this;
3 self.setInternalId = setInterval(this.getSum,2000);
4 },
5 beforeDestroy(){
6 let self = this
7 clearInterval(self.setInternalId);
8 console.log('消除定时器啦。。。。')
9 },
10 methods:{
11 getSum(){
12 let sum = 0,i=1;
13 while(i<100000000){
14 sum+=i++;
15 // sum=parseInt(sum/2)
16 }
17 let now = new Date();
18 console.log(sum,'秒数:',now.getSeconds());
19 }
20 }
我持续观察了半个多小时物理内存的变化,发现内存是时增长时减低,增长或降低的幅度都不会很大。
3、增加http请求
后面我在里面增加一个htttp请求后台数据,发现物理内存是一直上升的,上升的幅度明显比匿名函数大,上升的速度也很快,所以可能是前端请求导致的内存泄露。同时为了校验是setInternal还是http请求导致的内存泄露,我做了以下的代码校验:
1 mounted(){
2 let self = this;
3 self.setInternalId2 = setInterval(()=>{
4 let now = new Date();
5 console.log('第一个:',now.getSeconds());
6 self.getAllTasks(1); // http请求
7 self.setInternalId3 = setInterval(()=>{
8 let sum =0,i=1;
9 while(i<1000000){
10 sum+=i++;
11 }
12 let now = new Date();
13 console.log('第二个:',now.getSeconds());
14 },3000)
15 },1000)
16 },
17 beforeDestroy(){
18 let self = this;
19 clearInterval(self.setInternalId2);
20 clearInterval(self.setInternalId3);
21 console.log('消除计时器。。。。。')
22 },
发中间的setInternal计时器删掉后,物理内存消耗还是持续快速的增长。后面查资料得知,浏览器对单窗口的http请求数量是有限制的,谷歌浏览器可以并发执行最多6个,所以会导致http请求阻塞,从而消耗内存。
总结:这两个测试并不能证明是匿名函数所引起的内存泄露,很有可能是不断重复执行 ajax 引起的,但是我目前还查不到相关的资料证明,这个问题还有待考察。
三、常见的内存泄露
1、全局变量
在浏览器的环境下,全局变量对象就是window,定义全局变量如下:
1 function index(){
2 bar = "dsasd" ;
3 }
4 //上面代码相当于
5 function index(){
6 window.bar = "dsasd";
7 }
如果定义变量的时候忘记加上let或var,这时一个全局变量就会被创建出来,还有另一种定义全局变量
1 function foo(){
2 this.variable= "potential accidental global";
3 }
4 // 函数自身发生了调用,this 指向全局对象(window),(这时候会为全局对象 window 添加一个 variable 属性)而不是 undefined。
5
6 foo();
解决办法:为了防止这种错误的发生,在 JavaScript 文件开头添加 'use strict';
语句。这个语句实际上开启了解释 JavaScript 代码的严格模式,这种模式可以避免创建意外的全局变量。
2、定时器和回调函数
定时器造成内存泄露的主要原因是周期函数一直在运行,处理函数并不会被回收(只有周期函数停止运行之后才开始回收内存)。如果周期处理函数不能被回收,它的依赖程序也同样无法被回收。这意味着一些资源,也许是一些相当大的数据都也无法被回收。
观察者即监听事件,当它们不再被需要的时候(或者关联对象将要失效的时候)显式地将他们移除是十分重要的。现在,当观察者对象失效的时候便会被回收,即便 listener 没有被明确地移除,绝大多数的浏览器可以或者将会支持这个特性。尽管如此,在对象被销毁之前移除观察者依然是一个好的实践。
3、DOM 之外的引用
4、闭包
内存泄露可以详细看:https://blog.csdn.net/fay462298322/article/details/53172176