在js本身的gc机制,可以自动释放内存,但是无论是通过计数引用法,或者是标记清零法,都不能完全的避免内存泄露,前端js内存泄露,可以通过刷新浏览器来人为避免,但是nodejs在服务器端的内存泄露呢,定时重启服务器是一种方法,但是更好的是尽量的去避免内存泄露
- 本文先分析了gc机制,以及内存泄露的原因,最后总结了避免内存泄露的方法
1.javascript的内存回收机制(gc机制)
(1)计数引用法
语言引擎有一张”引用表”,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
来举一个计数引用的例子:
const name="yuxl";
name="jonyyu"
分析一下,这里第一句我们将name赋值为“yuxl”,此时,“yuxl”的计数加1从0——>1,这样“yuxl”就存在了内存里面,第二句又将name赋值为“jonyyu”,此时“yuxl”的计数减1,从1变为了0,“jonyyu”的计数从0变为1,此时因为“yuxl”的计数为0,所以“yuxl”就从内存中被释放。
(2)标记清除法
- 当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”
- 垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了
var a="aa";
function test(){
var b="bb";
console.log(a)
console.log(b);
}
test();//输出“aa”,“bb”
console.log(b)//输出“undefined”
我们来分析这个函数,调用test()的时候,进入test的执行环境,此时环境中存在了变量b,因为test函数的上下文中可以访问到全局变量a(根据函数的作用域原理),因此输出了“aa”,“bb”,当执行完这个函数以后,变量b标记被清除,也就是b被释放,因此在此后的console.log的语句中,b为undefined。
(3)计数引用法带来的内存泄露问题
计数引用法可能带来内存泄露,因此在js的gc机制中没有大量采用,比如:
function test(){
var a={};
var b={};
a.prop=b;
b.prop=a;
}
test()
从上面的例子中,如果a,b中的属性相互引用,那么在函数运行后,如果采用计数引用法,它们之间的计数都为1,因此不会被清除。
2.通过WeakMap和WeakSet来解决计数引用法带来的内存泄露问题
虽然较少的浏览器采用了计数引用法来实现Js的gc机制,下面我们来看一下如何在计数引用法的情况下避免内存泄露的问题。
其原理就是:
对于某些情况下的引用,可以不计数,不计入引用次数。
具体实现方式:WeakMap,WeakSet,他们对于值的计数都不会计入垃圾回收机制。举例来说:
const wm=new WeakMap();
const element=document.getElementById('test');
wm.set(element,'some message');
wm.get(element);//可以得到值为“some message”
这是一个简单的例子,如果来看,此时:
element=null
这样,令element为空,此时如果通过process.memoryUsage()来观察会发现内存已经被清空了。
3.内存泄露的几种原因
(1)全局变量
a = 10;
//未声明对象。
global.b = 11;
//全局变量引用
(2)闭包
function test(){
var x=10;
return function test_child(){
console.log(x)
}
}
调用test()后,x变量没有被释放。
(3)事件监听
Node.js 的事件监听也可能出现的内存泄漏。例如对同一个事件重复监听,忘记移除(removeListener),将造成内存泄漏。
(4)其他原因
有一些其他的情况可能会导致内存泄漏,比如缓存。在使用缓存的时候,得清楚缓存的对象的多少,如果缓存对象非常多,得做限制最大缓存数量处理。还有就是非常占用 CPU 的代码也会导致内存泄漏,服务器在运行的时候,如果有高 CPU 的同步代码,因为Node.js 是单线程的,所以不能处理处理请求,请求堆积导致内存占用过高。
4.如何避免内存泄漏
- ESLint 检测代码检查非期望的全局变量。
- 使用闭包的时候,得知道闭包了什么对象,还有引用闭包的对象何时清除闭包。最好可以避免写出复杂的闭包,因为复杂的闭包引起的内存泄漏
- 绑定事件的时候,一定得在恰当的时候清除事件。在编写一个类的时候,推荐使用 init 函数对类的事件监听进行绑定和资源申请,然后 destroy 函数对事件和占用资源进行释放