1、是什么?
内存泄露(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,造成应用程序卡顿或崩溃,轻则影响系统性能,重则导致进程崩溃。
2、垃圾回收机制
JavaScript具有自动垃圾回收机制(Garbage Collection),即执行环境会负责管理代码执行过程中使用的内存。
原理:垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。
两种方式:
(一)标记清除--JavaScript最常用的垃圾回收机制
当变量进入环境时,就标记这个变量为”进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则标记为”离开环境“。
垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它将所有在上下文中的变量、以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并回收它们的内存。
var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
a++
var c = a + b
return c
}
(二)引用计数
语言引擎有一张”引用表“,保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。
如果一个值不再需要了,引用次数却不为0.垃圾回收机制无法释放这块内存,从而导致内存泄露。
例如:
const arr = [1, 2, 3, 4];
console.log('hello world');
//数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存。如果需要这块内存被垃圾回收机制释放,只需要设置如下:
arr=null
//通过设置arr为null,就解除了对数组[1,2,3,4]的引用,引用次数为0,就被垃圾回收了
小结:有了垃圾回收机制,不代表不用再关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用,如果是的话,则必须手动解除。
3、常见内存泄露情况
定时器、函数闭包、没有清理对DOM元素的引用、使用事件监听addEventListener监听的时候,再不监听的时候使用removeEventListener取消对事件的监听。
意外的全局变量:
function foo(arg){
bar='this is a hidden global variable'
}
由this创建的全局变量:
function foo(){
this.variable='potential accidental global variable'
}
foo()//foo调用自己,this指向了全局对象(window)
这两种情况可以通过使用严格模式(use strict)避免意外的全局变量。
定时器:
var someResource=getData()
setInterval(function(){
var node=document.getElementById('Node')
if(node){
//处理node和someResource
node.innerHTML=JSON.stringify(someResource)
}
,1000)
如果id为Node的元素从DOM中移除,该定时器仍会存在,同时因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放
闭包:
function bindEvent(){
var obj=document.createElement('XXX')
var unused=function(){
console.log(obj,'闭包内引用obj obj不会被释放')
}
obj=null//解决方法
}
var foo=bindEvent()
//理论上说,bindEvent()函数内部定义的变量应当在执行完bindEvent之后就销毁,但是foo引用了bindEvent方法,按照引用计数法,是不会回收bindEvent内部定义的变量的,这就会造成内存泄露。
没有清理对DOM元素的引用同样造成内存泄露:
const refA=document.getElementById('refA')
document.body.removeChild(refA)//dom删除了refA元素
console.log(refA,'refA')//引用refA,打印出整个dom元素
refA=null
console.log(refA,'refA')//解除引用
const wrapDOM = document.getElementById('wrap');
wrapDOM.onclick = function (e) {console.log(e);};
// some codes ...
// remove wrapDOM
wrapDOM.parentNode.removeChild(wrapDOM);
4、内存泄露的排查手段
Chrome浏览器打开F12,开始记录Performance查看js堆内存占用信息:
点击Stop停止录制。
选中JS Heap,下面展现出来的一条蓝线,就是代表了这段记录过程中,JS 堆内存信息的变化情况。
如果蓝线一直呈上升趋势,那么说明有很多未被释放的内存。至于这些内存是否泄露还是在使用,需要进一步排查。
Memory精确定位内存使用情况,点击按钮生成应用在当前时刻的内存快照信息。
当生成第一个快照的时候,开发者工具窗口显示了很详细的内存占用情况:
Constructor:占用内存的资源类型。
Distance:当前对象到根的引用层级距离
Shallow Size:对象所占内存(不包含内部引用的其它对象所占的内存)(单位:字节)
Retained Size:对象所占总内存(包含内部引用的其他对象所占的内存)(单位:字节)
每项展开就可以查看更详细的数据信息。可以再次切回网页,继续操作几次,然后再次生成一个快照:
选择Comparison可对两次快照进行更详细的比对:
#New:新分配的内存空间数
#Deleted:销毁的内存空间数
#Delta:内存回收差值=新分配-销毁,如果是正值,代表新生成的内存多,释放的内存少。其中的闭包closure项如果是正值,说明存在内存泄露
5、内存泄露的解决办法
5.1、使用严格模式,避免不经意间的全局变量泄露:
use strict
function foo(){
b=2
}
foo()// ReferenceError: b is not defined
5.2、关注DOM生命周期,及时销毁DOM
const wrapDOM = document.getElementById('wrap');
wrapDOM.onclick = function (e) {console.log(e);};
// some codes ...
// remove wrapDOM
wrapDOM.onclick = null;
wrapDOM.parentNode.removeChild(wrapDOM);
或者可以使用事件委托的手段统一处理事件,减少由于事件绑定带来的额外内存开销:
document.body.onclick = function (e) {
if (isWrapDOM) {
// ...
} else {
// ...
}
}
5.3、避免过度使用内包
大部分的内存泄漏还是由于代码不规范导致的。代码千万条,规范第一条,代码不规范,开发两行泪。