大家是不是一看到内存泄漏这几个字就怂了?别怕别怕,其实这玩意根本就不可怕,且听我细细道来。。。
很多语言里面的内存回收都是手动回收的。但是写过js的都知道,好像并没有怎么太关心过内存回收这回事,以至于很多人都不知道js有内存回收这回事。其实js也是有内存回收的,只不过这个过程是自动的,被一个叫做gc(garbage collection)的家伙收走了。
gc就像家里的吸尘器机器人一样,让你完全不用担心家里的垃圾,专心做你专注做该做的事。
首先我要说明一下,gc其实可以帮你搞定绝大部分的垃圾回收,因此你没有必要处处小心内存泄漏的问题,就算有一些,也不会有多大影响。基础
把大象放进冰箱需要几步?答案是3步:打开冰箱,放入大象,关上冰箱。
同理,内存的使用也是三步。开辟内存,使用内容,回收内存。
我们平常的定义变量其实就是开辟内存,开辟完之后,该变量会指向那个地址。
开辟之后就是使用了,使用就是对内存的读取或者写入,传递参数也算。现在到了最关键的时刻,如何自动回收呢?
gc自动回收最主要的评判标准:没有变量引用的地址会被回收。就好比国家征收土地,发现了一块地皮,然后问这是谁的,然后没有人认领这块土地,那么这块土地自然就被国家没收了。
先来个小例子来看一下这个玩意。
上面的例子中,a开始指向一个地址,可是后来把a指向了另外一个地址,因此之前的地址就变成一个无人认领的地皮,就被gc回收了。为了验证它,这里有一个demo:
页面很简单,上面就是一个按钮,作用就只有一个,就是将全局变量a={name:'zc'}变成'12'。
现在我们打开chrome developer中的memory面板,在点击按钮前后拍摄内存快照。
我们打开其中的(string),看看有什么变化。
当然,我们也可以在(object)中查看刚才被回收的那个对象。
不要问我怎么发现(string),(object)中有的,我也是自己找了好久才发现的...我发现,一般在(string)里面找东西比较好找,还有就是可以在(closure)中查找function。
还有一点要注意,这个memory面板不完全可信,因为内部的gc回收逻辑我们不是十分清楚,例如,将a变成1的话,你就有可能看不到增加的地址(正常可以在(number)里面看到),可能是变化的太小,这个时候你可以变大一点,如a=1234123423,这样就能看到增加的地址了。
实战分析
上面大概介绍了gc,以及gc的主要回收方法。现在我们就利用刚才的方法是看看实际中的内存泄漏问题。
1.jquery的html('')是否会导致泄漏?
不知道大家有没有这样的疑问:我在一个对象上用jquery注册了一个事件,然后我直接对它的父容器用html('')将内容进行全部替换,那个对象上注册的方法是否会被释放掉?
为了验证它,我设计了一个demo,由一个out(父容器)和inner(子元素)组成,给inner绑定了click和dblclick事件,点击button就对out进行html(''),看最后会不会将inner上的事件释放掉。因为一般用on注册的,必须用off销毁,但是这里没有off,而是直接父容器html('')。
同样的,我们在点击btn之前和点击之后分别来个快照,并且选择对比模式,我们看看有没有什么变化。
打开(closure),可以看到最下面两个是被销毁的click方法和dblclick方法。因此,直接对父容器html(''),gc会自动回收子元素的方法。
结论:html('')方法不会导致子元素事件的泄漏。
2.如果两个对象相互引用对方,是否会造成内存泄漏?
问:两个对象的相互引用有什么特殊吗?为什么要用它来测试?
答:你还别说,这玩意确实挺特殊的。按照我们上面的理论,如果一个对象没有变量引用就会被回收,那么假如要被回收的地址(开始指向它的变量突然指向别的地方了)有人指向(这里指向的它的就是和它一起循环的另一半)是否会被回收?
这个分为两种情况,一种是局部变量的循环引用,一个是全局变量的循环引用,一个一个来,我们先看局部变量的。
局部变量的循环引用:
我们定义了一个方法fuck,里面有两个变量,界面上有一个按钮,每次点击都会执行fuck。
var fuck = function() {
var a={
link:b,
name:'aaaa'
};
var b={
link:a,
name:'bbbb'
}
}
现在我们在点击btn之前和点击后第一次和第二次分别快照,照三次。为什么照三次?因为我们要看是否每次点击都会造成内存泄漏。
我们发现第一次点击后确实是增加了内存,但是第二次为什么没有反应?没有新的内存增加?也没有内存减少?其实经过我多次的测试,发现如果是局部里面的变量,好像只是会第一次开辟内存,再次点击就直接用了,不新增也不减少。因此,局部变量的循环并不会造成内存泄漏。
全局变量的循环引用:
和上面基本类似,不同的是把变量提出来了。
var a,b;
var fuck = function() {
a={
link:b,
name:'aaaa'
};
b={
link:a,
name:'bbbb'
}
}
同样是三张快照,让我们来看看不同。
我们可以看到,除了第一次的初始化之外,第二次也增加了对象,增加了b对象和a对象以及a.link对象。(为什么a增加了两个对象而b之增加了一个?因为a先定义的,定义的时候还没有b,因此a.link又开辟了内存。)
来个图就明白了:
看到没,全局变量如果相互引用的话,就会因为有指向而无法释放。因此,全局变量的循环引用会导致内存泄漏。
3.自己引用自己
这个是互相引用的加强版,自己引用自己。我们这次也主要是考虑全局变量的自己引用自己,因为局部的只会初始化一次而已。
var a;
var fuck = function() {
a={
link:a,
name:'aaaa'
};
}
同样点击btn前和点击后一次,两次拍三次快照。
看到没,自引用同样有内存泄漏问题,随着你点击的增多,越来越多的内存无法释放。
不过,这里要再给大家讲两个类似的变种问题。
第一种情况:link写在外面,而不是定义里。
var a;
var fuck = function() {
a={
name:'aaaa'
};
a.link=a;
}
注意哦,这种情况和直接写在里面不同,不信就来个快照看看。
看到没,第二次的时候,先开辟了内存,然后释放了上次的内存。这种情况不会导致内存泄漏。
第二种情况:利用方法指向自己。
var a;
var fuck = function() {
var _a=a;
a={
link:function(){
if(_a){}
},
name:'aaaa'
};
}
我们在方法的内部定义个一个变量指向a,来看看情况。
看到没,利用方法指向自己也会导致内存泄漏。(注意了,如果没有用_a指向a,那么则不会导致内存泄漏,这个感兴趣的小伙伴可以自己去试试看。)
4.DOM泄漏
DOM泄漏比上面的泄漏更加的明显而且更加的好找。为了讲解方便,我们就从增加元素和删除元素两个方面来看看DOM泄漏问题。
增加元素:
var a;
var fuck = function() {
a=$('
}
看一下快照的结果:
第一次点击的时候,多出了一Detached DOM ,第二次点击的时候创建了新的,同时旧的被回收了,因此,如果全局变量创建DOM的话,第一次会内存泄漏,创建一个无用的DOM。
删除元素:
删除元素的内存泄漏主要表现在引用了一个元素,但是把它的父容器给删除了。
为了验证,我们定义了一个父容器tree,子元素leaf,分别用变量引用他们,然后把父容器删除。
var $tree=$('#tree');
var $leaf=$('#leaf');
$('#btn').on('click', function() {
$('#tree').remove();
})
解决方案很简单,第一种就是不要用全局变量去定义,去局部变量定义的话,不会生成Detached Dom,第二种就是将父容器删除后,将$leaf和$tree都设置成null,这样就会自动回收了。最后
好吧,讲了这么多,不知道你记住多少呢?本文的目的不是告诉你什么什么定理,而是告诉你们怎么去做,怎么去分析。当然本文的分析也不见得正确,完全是个人理解,山外有山,人外有人,如果你有什么高见,欢迎指出讨论。
最后再说一句,因为gc帮我们做了大部分的事情,所以我们其实在写的时候没有必要担心什么内存啊之类的。上面列举的例子中,但凡出现泄漏的主要都是全局变量并且是自己引用自己或者循环引用导致的,只要你写的时候稍微注意一下,问题就不大,当然,还有我没讲到的特殊例子,到时候遇到了,再和大家分享一下。