垃圾回收机制
垃圾的产生与回收
- js 在运行时所创建的变量会动态的存在内存中。
- js 内存管理中有个词叫
可达性
,指可被访问或者可用的值,它们保证存在内存中。相反,不可达的就是需要被回收的内存(变量) - js 垃圾回收机制会定期(实时开销太大)找出那些不再用到的变量进行回收,释放内存。
回收策略
标记清除
策略
标记清除算法被大多数浏览器采用,不同浏览器厂商对此算法进行了优化且不同浏览器的 js 引擎在垃圾回收的频率上有所差异。
标记方法
- 当变量进入执行环境,反转某一位(用二进制字符标记)
- 维护进入时环境变量和离开时环境变量两个列表
大概步骤
- 初始时垃圾收集器假设所有变量都需要收集,给所有变量标记为 0
- 从根对象开始遍历,把不是垃圾的变量反转为 1
- 回收标记为 0 的变量,释放内存
- 把剩下的变量反转回 0 等待下轮垃圾回收
优点
实现起来简单
缺点
在垃圾清除后,剩余变量地址是不变的,这时就会导致内存碎片化,引发内存分配问题
由于内存是不连续的,所以每次为变量分配内存都会遍历内存列表寻找等于大于它的内存
常见三种内存分配方法
First-fit
,找到第一块适合的内存块并立马返回Best-fit
,遍历空闲内存块找到适合的且最小的内存块并返回Worst-fit
,遍历空闲内存块找到最大的一块,将其分成两块,一块是适合的内存块并返回它
Worst-fit
利用率看起来更合理,但实际上会分出更多小块,不推荐使用;考虑到分配的速度和效率First-fit
比Best-fit
更合适,
综上,标记清除算法的主要缺点为内存碎片化和分配速度慢(即使是First-fit
策略,最坏情况是每次都要遍历到最后)
而标记整理算法可以有效解决这两个问题,它会在标记后将不需要清理的内存块向一侧移动,然后清理掉边界内存
引用计数
策略
这是浏览器早期的一种垃圾回收算法,它将内存是否需要回收简单定义为对象是否被其他对象所引用。当引用次数为 0 时就会回收掉这个对象。由于会出现循环引用问题,现在很少使用这种方法了。
它的策略是跟踪记录每个变量的值被使用的次数:
- 当声明一个变量并且将一个引用类型赋值给它时,引用数为 1
- 当同一个值被赋值给另一个变量时 ,引用数加 1
- 当该变量的值被其他值覆盖时 ,引用数减 1
- 当引用数为 0 时,说明没有被变量使用,其值被内存回收
js
let a = new String(""); // 被a引用,此对象引用数为1
let b = a; // 又被b引用,引用数加 1,此对象引用数为2
a = null; // 只有b引用,此对象引用数为1
b = null; // 没有变量引用,此对象引用数为0;被回收
循环引用的例子:
js
let a = new String(""); // 对象1被a引用
let b = new String(""); // 对象2被b引用
a.obj = b; // 对象2被a引用
b.obj = a; // 对象1被b引用
优点
- 当引用计数为 0 时会立马被回收
- 标记清除算法会定时遍历堆内存里的活动对象和非活动对象来进行垃圾回收(会阻塞 js 线程),而引用计数只需在引用时计数就行。
缺点
- 需要一个很大的计数器,因为我们也不知道计数的上限
- 被循环引用的值无法被回收
内存泄露
简而言之,当不再用到的对象没有被回收时,它被称为“内存泄露”。所以我们的代码应避免一些不利于垃圾回收的操作。
举个栗子
js
function fn1() {
let list = new Array(1000).fill("str");
return function () {
console.log("ccc");
};
}
let fun = test();
fun();
这个例子中,虽然fun
是个闭包,但他没有引用fn1
的内部变量,所以fn1
里面的变量可以被回收,因此没有造成内存泄漏
js
function fn2() {
let list = new Array(1000).fill("str");
return function () {
console.log("ccc");
return list;
};
}
let fun = test();
fun();
而这个例子中,fun
引用了fn2
的内部变量list
,所以fn2
里面的list
无法被回收,从而造成内存泄漏
针对这个例子我们只需在fun
调用完将它置空就可以避免内存泄露问题
js
fun = null;