垃圾回收机制(GC)

为什么需要垃圾回收机制

程序工作过程中会产生很多数据,但是有些数据可能用过一次后就不用了,如果不对这些数据进行清除,就会占用很多内存空间。有可能导致进程崩溃。
所以就需要垃圾回收机制就是对不需要用到的数据进行删除,释放内存。

什么是垃圾

在 JavaScript 内存管理中有一个概念叫做可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收。垃圾回收机制就是定期找出那些不再用到的内存,然后释放其内存。

算法策略

最为常见的就是:标记清除算法、引用计数法

引用计数法

思路:维护一张引用计数表,如果某个值被引用了,则他的引用数就+1,减少就-1,一旦变为0,则该值会被立即回收。
优点: 发现垃圾立即回收,使内存得到有效利用;
缺点:
无法回收循环引用的对象;
时间开销大,因为引用计数算法需要维护引用数,一旦发现引用数发生改变需要立即对引用数进行修改。
例子:

function aa() {
    let a = new Object();
    let b = new Object();
    a.c = b;
    b.c = a;  //互相引用
}

在这里插入图片描述

标记清除算法

该算法分为两步:标记和清除
标记阶段:为所有活动对象打上标记
清除阶段:销毁所有没有打标记的非活动对象
过程:

  • 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
  • 然后从各个根对象开始遍历,把能遍历到的节点标记为1
  • 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
  • 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
    优点: 标记清除算法的优点只有一个,那就是实现比较简单,打标记也无非打与不打两种情况,使用一位二进制位(0和1)就可以做到。
    缺点: 标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片(如下图),导致内存分配时间延长了。
    内存分配问题

V8的GC

V8引擎就用到了标记清除算法,不过对于改算法进行了优化加工处理。
V8引擎采用的分代式回收策略
回收策略:V8采用的S是可访问性(reachability)算法来判断对象是否是活动对象,这个算法是将一些GC Root(根对象)作为初始存活的对象的集合,从GC Roots对象出发,遍历GC Root中的所有对象:

  • 通过GC Root能访问到的对象,我们就认为该对象是可访问的,那么必须保证这些对象应该在内存中保留,这些对象为活动对象
  • 通过GC Roots不能访问到的对象就可能被回收,这些对象为非活动对象
    GC Root有很多,通常包括了以下几种:
  • 全局对象window、global
  • DOM树,由可以通过遍历文档到达的所有原DOM节点组成
  • 存放在栈上变量

什么是分代式算法

将堆内存分为新老生代两个区域,新生代存放存活时间较短的对象;
而老生代存放存活时间较长或者常驻的对象。
对于新老生代V8采用两种垃圾回收器来管控。
在这里插入图片描述

新生代(Young Space)的垃圾回收机制

采用Scavenge算法,将堆内存分为使用区和空闲区。顾名思义,
使用区(from space):处于使用状态的空间
空闲区(to space):处于闲置状态的空间
当使用区快被写满是,就执行一次垃圾清理操作。
新生代

新建一个对象
重新赋值
Scavenge算法大概原理
它会维护两个指针,allocationPtr(allocation pointer 配置指针)、scanPtr(scan pointer 扫描指针)

  • allocationPtr:指向新对象要复制到的地方
  • scanPtr:指向即将要进行扫描的对象
    比如现在有一个数据结构是这样的:
var A = {C: {}};
var B = {
 D: {
     F: {},
     G: {}
 },
 E: {}
};
delete A.C;

在这里插入图片描述
使用Scavenge算法
在这里插入图片描述

  • 优点: 只复制活动对象,生命周期短的场景种活动对象只占很少一部分,所以执行效率很高;复制过程中就能完成内存整理,避免产生内存碎片
  • 缺点: 浪费一半新生代的空间

新生代对象如何变成老生代对象:

晋升策略
在这里插入图片描述
文字解释:

  • 当一个对象经过一轮垃圾回收后依然存活,就会被认为是生命周期较长的对象,随后被移入老生代中
  • 如果复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中。(设置为 25% 的比例的原因是,垃圾回收器回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配)

问题:
1、复制进空闲区进行排序后,对象存放的地址改变了,那引用它的参数的值也得跟着改变,那么这是怎么实现的。如何实现反向知道引用它的参数,并修改值

老生代(Old Space)的垃圾回收机制

老生代垃圾回收器采用的是标记整理算法,该算法是基于标记清除算法进行优化。
标记清除算法会产生大量的内存碎片,而标记整理算法就解决了这一问题
标记整理算法
在标记清除法的基础是,对内存空间进行整理
标记清除

整理

全停顿(Stop-The-World)

为了避免js执行和垃圾回收同时进行倒是内存数据位置的变动,所以当垃圾回收器运行时,js执行会暂停下来,等到垃圾回收结束后再继续运行;
缺点: 如果垃圾回收占用时间较长页面就会出现明显的卡顿。

V8垃圾回收的优化策略

针对全停顿问题,V8通过添加并行、增量、并发等对垃圾回收机制进行优化。

并行回收(Parallel)

在主线程执行垃圾回收任务的同时,开启多个辅助线程并行处理,加快垃圾回收的执行速度。
并行回收

增量回收(Incremental)

主线程中,js和垃圾回收交替执行,可以避免单次垃圾回收时间过长造成的卡顿问题。
在这里插入图片描述
增量回收 (Incremental)会带来两个问题:

  • 垃圾回收和js切换执行,暂停垃圾回收时需要保存当时的标记结果,切换回来之后需要知道从哪个位置继续执行
  • 切换到js执行后,js代码的执行可能会修改之前已经标记好的数据,造成影响
    针对上面的两个问题,V8引入了三色标记法和写屏障机制(Write-barrier)来解决。

三色标记法

V8采用的是黑、白、灰三色标记法。

  • 黑色:表示这个节点已经被访问到了,而且该节点的子节点都已经标记完成了。
  • 灰色:表示这个节点被访问到了,但子节点还没被标记处理,也表明了目前正在处理这个节点。
  • 白色:表示这个节点没有被访问到,如果在本轮垃圾回收结束时还是白色,那么这块数据就会被收回。初始阶段,所有节点都是白色。
    垃圾回收器可以根据当前是否存在灰色节点来判断整个标记是否完成。如果没有灰色节点了,就可以清理掉白色节点了。如果还有灰色标记,当再次恢复垃圾回收时,便从灰色的节点开始继续执行。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

写屏障机制

问题: 垃圾回收器将某个节点标记为黑色后,js代码执后又为该黑色节点增加了一个节点,由于新增节点都是白色,垃圾回收器不会再次将这个白色节点进行标记了,它就会被垃圾回收器回收。

解决: 强制将被引用的节点标记为灰色
例如:

window.a = Object()
window.a.b = Object()
window.a.b.c=Object()
window.a.b = Object() // d 

在这里插入图片描述
在执行object.field = value时,V8就插入写屏障代码,强制将value标记为灰色。

并发回收 (Concurrent)

并发是指主线程不断执行js代码,而辅助线程则在后台完全执行垃圾回收。
在这里插入图片描述

并发回收主要有以下两个问题:

  • 当主线程执行js代码时,堆中的内容随时都有可能发生变化,从而使得辅助线程之前做的工作完全无效
  • 主线程和辅助线程极有可能在同一时间去更改同一个对象,这就需要额外实现读写锁的一些功能

V8的实现

在这里插入图片描述
新生代对象空间: 采用并行策略,在整理排序阶段,也就是将活动对象从from-space复制到to的时候,启用多个辅助线程,并行的进行整理。
老生代对象空间: 主要使用并发标记,主线程在开始执行 JavaScript 时,辅助线程也同时执行标记操作(标记操作全都由辅助线程完成);标记完成之后,再执行并行清理操作(主线程在执行清理操作时,多个辅助线程也同时执行清理操作);同时,清理的任务会采用增量的方式分批在各个 JavaScript 任务之间执行。

内存泄漏

常见的内存泄漏场景

  • 意外声明全局变量
    未声明的对象会被绑定在全局对象上,就算不被使用了,也不会被回收,所以写代码的时候,一定要记得声明变量。
function hello (){
    name = 'tom'
}
hello();
  • 定时器
    定时器的回调通过闭包引用了外部变量,如果定时器不清除,name会一直占用着内存,所以用定时器的时候最好明白自己需要哪些变量,检查定时器内部的变量,另外如果不用定时器了,记得及时清除定时器。
let name = 'Tom';
setInterval(() => {
  console.log(name);
}, 100);
  • 闭包
    由于闭包会常驻内存,在这个例子中,如果out一直存在,name就一直不会被清理,如果name值很大的时候,就会造成比较严重的内存泄漏。所以一定要慎重使用闭包。
let out = function() {
  let name = 'Tom';
  return function () {
    console.log(name);
  }
}
  • 事件监听
    在页面初始化时绑定了事件监听,但是在页面离开的时候未清除事监听,就会导致内存泄漏。
mounted() {
window.addEventListener("resize",  () => {
    //do something
});
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值