前言
学习并使用闭包的时候总会在各博客里面看到闭包的坏处有一条:
使用不当的闭包将会在IE(IE9之前)中造成内存泄漏
uhhh,为什么在IE9之前会造成这样的结果呢?我就开始继续寻找答案,找到一条比较满意的:
IE9的JavaScript引擎使用的垃圾回收算法是引用计数法,对于循环引用将会导致GC无法回收“应该被回收”的内存。造成了无意义的内存占用,也就是内存泄漏。
先科普一下:内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
什么是GC
GC是Garbage Collection的缩写,意为垃圾回收
这些垃圾是程序不用的内存空间(可能是在之前用过了,以后不会再用的)。那么GC就是负责收走垃圾的,因为他工作在JavaScript引擎内部,所以对于我们前端开发者来说,GC在“一定程度上”是悄无声息工作的(注意此处的加引号部分)
那么,我们明确了GC做什么了:
- 找到内存空间中的垃圾。
- 回收垃圾,让程序员能再次利用这部分空间。
这里要注意的是,不是所有语言的世界里面都有GC,相对来说,高级语言里面一般会带GC,比如Java,JavaScript,Python,在没有GC的世界里,需要程序员手动管理内存,比如C语言我们常见的malloc/free,其实就是memory allocation的缩写。当然,还有C++里面的new/delete。
常用的几种GC算法
1.引用计数法
顾名思义,让所有对象实现记录下有多少“程序”在引用自己,让各对象都知道自己的“人气指数”。举一个简单的例子:
var a = new Object(); // 此时'这个对象'的引用计数为1(a在引用)
var b = a; // ‘这个对象’的引用计数是2(a,b)
a = null; // reference_count = 1
b = null; // reference_count = 0
// 下一步 GC来回收‘这个对象’了
复制代码这个方法有优势也有劣势:
优点
- 可即刻回收垃圾,当被引用数值为0时,对象马上会把自己作为空闲空间连到空闲链表上,也就是说。在变成垃圾的时候就立刻被回收。
- 因为是即时回收,那么‘程序’不会暂停去单独使用很长一段时间的GC,那么最大暂停时间很短。
- 不用去遍历堆里面的所有活动对象和非活动对象
缺点
- 计数器需要占很大的位置,因为不能预估被引用的上限,打个比方,可能出现32位即2的32次方个对象同时引用一个对象,那么计数器就需要32位。
- 最大的劣势是无法解决循环引用无法回收的问题 这就是前文中IE9之前出现的问题
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2,o2的引用次数是1
o2.a = o; // o2 引用 o,o的引用此时是1
return "azerty";
}
f();
fn在执行完成之后理应回收fn作用域里面的内存空间,但是因为o里面有一个属性引用o2,导致o2的引用次数始终为1,o2也是如此,而又非专门当做闭包来使用,所以这里就应该使o和o2被销毁。
因为算法是将引用次数为0的对象销毁,此处都不为0,导致GC不会回收他们,那么这就是内存泄漏问题。
该算法已经逐渐被 ‘标记-清除’ 算法替代,在V8引擎里面,使用最多的就是 标记-清除算法
2.标记清除算法
主要将GC的垃圾回收过程分为两个阶段
- 标记阶段:把所有活动对象做上标记。
- 清除阶段:把没有标记(也就是非活动对象)销毁。
标记阶段:
根可以理解成我们的全局作用域,GC从全局作用域的变量,沿作用域逐层往里遍历(对,是深度遍历),当遍历到堆中对象时,说明该对象被引用着,则打上一个标记,继续递归遍历(因为肯定存在堆中对象引用另一个堆中对象),直到遍历到最后一个(最深的一层作用域)节点。
标记完成之后,就是这样的:
清除阶段
又要遍历,这次是遍历整个堆,回收没有打上标记的对象。
这里我们不细讲如何将获得的内存空间再分配的问题,这个地方有点类似磁盘管理或者内存管理,比如best-fit,First-fit,Worst-fit。以及碎片化问题的产生和解决方法。
这种方法可以解决循环引用问题,因为两个对象从全局对象出发无法获取。因此,他们无法被标记,他们将会被垃圾回收器回收。正如图:
优点:
- 实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示
- 解决了循环引用问题
缺点
- 造成碎片化(有点类似磁盘的碎片化)
- 再分配时遍次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端
这种GC方式是一个定时运行的任务,也就是说当程序运行一段时间后,统一GC,类似如图:
3.复制算法
复制算法配合这张图理解起来非常简单,就是只把某个空间的活动对象复制到其他空间。
将一个内存空间分为两部分,一部分是From空间,另一部分是To空间,将From空间里面的活动对象复制到To空间,然后释放掉整个From空间,然后此刻将From空间和To空间的身份互换,那么就完成了一次GC。
如图所示:
原文链接:https://juejin.cn/post/6844903556265279502