js垃圾回收机制
堆内存 栈内存
基本类型变量名以及值都存放于栈内存
引用类型变量值存放于堆内存,变量名以及堆内存地址存放于栈内存
V8 的内存分代
主要将内存分为新生代和老生代,新生代指的是那些存活时间较短的对象,老生代指的是存活时间较长的或者常驻内存的对象。
垃圾的来源
全局变量
全局变量都不会是垃圾,因为该变量可能会被后面的代码所用
局部变量
局部变量在函数执行完就会被回收
window 上的变量
与全局变量同理
对象 开辟的堆内存
dom 节点 通过 getElement 等获取到的节点
垃圾的回收算法
javascript 的垃圾回收会对 javascript 执行线程形成阻塞
引用计数(已被大部分浏览器弃用)
使用计数器实时记录对象的引用计数,当计数为 0 时,即会被回收
|
优点
- 可即时回收垃圾
- js 堵塞时间短
- 无需沿着指针去一遍一遍找
缺点
实现繁琐,每个赋值必须进行引用更新
循环引用无法回收
var obj1 = {}; //此时开辟了一个堆内存,我们标记它{}为X,X的引用计数为1
var obj2 = {}; //此时开辟了一个堆内存,我们标记它{}为Y,Y的引用计数为1
obj2.a = obj1; // Y: {a:X} X的引用计数为2 (内部引用)
obj1.a = obj2; // X: {a:Y} Y的引用计数为2 (内部引用)
//形成了环形引用
obj1 = null; // X的引用计数为1
obj2 = null; // Y的引用计数为1
//此时X Y无法被释放
标记-清除(标记-整理-删除)(现代浏览器基本用这种)
定期的从这个全局对象(window||global)开始,递归查找引用的对象,在扫描过程中采用引用计数,如果能访问到,就表示活着,不会被释放,然后将散乱的可用内存进进行整理,使得到更多的连续可用内存
缺点:
- 标记效率慢,垃圾回收每次都要去遍历检查每一个引用
优化点:
- 针对新生代和老生代,采用不同的算法,老生代回收不用那么勤快,而新生代需要马上标记马上回收。
- 增量执行(主要老生代):将回收过程分成许多一小份,垃圾回收与应用逻辑交替执行直到标记阶段完成。
- 延迟清理:在 js 空余的时间再进行回收
算法:
Scanvenge(新生代)(无整理)
将新生代的内存一分为二,From 区和 To 区,用来存放对象的一半是 From 空间,处于闲置状态的一半是 To 空间。
该算法缺点就是只能用一半的内存
新生代和老生代如何区分
对象刚创建时即为新生代,而新生代到老生代的过程称为晋升,晋升条件为:
- 对象经历过一次 Scanvenge 算法仍存在
- To 空间的内存占用超过限制
那么老生代的定义可为:存活周期较长或常驻内存的对象,或为新生代对象回收中溢出的对象
Mark-Sweep&Mark-Compact(老生代) (标记—整理—清除)
老生代空间大,大部分都是活着的对象,GC 耗时比较长
垃圾回收时,并不会一分为二,首先是标记阶段。V8 会在标记阶段遍历老生代空间中的所有对象,并标记存活的对象(即还没有被完全释放的对象),Mark-Compact 将存货的整理到一边,在随后的清除阶段,会将所有未标记的老生代对象全部回收。
Mark-Sweep
Mark-Compact
三种回收算法对比
回收算法 | Mark-Sweep | Mark-Compact | Scavenge |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少 | 少 | 双倍空间(无碎片) |
是否移动对象 | 否 | 是 | 是 |
内存泄露
即一些内存本来时没用的,但是无法被回收
- 没用全局变量
- 大量闭包
所以,我们要通过 delete 或重新赋值变量为 undefind 或者 null 来释放对象的引用。
WeekMap 和 WeekSet
顾名思义 Week 也就是说,是一种弱类型的 Map 和 Set,主要体现在垃圾回收和使用上。
使用
- WeekMap 的 key 只能是object,null,undefine,不能是原始值。
- WeekMap 不支持迭代以及 keys(),values() 和 entries() 方法,没有办法获取 WeakMap 的所有键或值。
- 保留 get、set、delete、has
- 同理 WeekSet 只能添加object,null,undefine,不能是原始值,不支持迭代,以及 size()和 keys()
- 保留 add、has、delete
垃圾回收
WeekMap 和 WeekSet 都是弱引用,不会妨碍垃圾回收,也就是说存在其中的数据,在没有其它引用时,会及时被回收。
使用场景
- 保存节点等 object,防止内存泄露,及时进行垃圾回收