浅谈v8垃圾回收

前言
在 JavaScript 中我们知道,原始数据类型是存储在栈空间中的,引用类型的数据是存储在堆空间中的。
不过有些数据被使用之后,可能就不再需要了,我们把这种数据称为垃圾数据。如果这些垃圾数据一直保存在内存中,那么内存会越用越多,所以我们需要对这些垃圾数据进行回收,以释放有限的内存空间。
JavaScript 的数据是如何回收的
栈回收
因为数据是存储在栈和堆两种内存空间中的,所以接下来我们就来分别介绍“栈中的垃圾数据”和“堆中的垃圾数据”是如何回收的。首先是调用栈中的数据:
当函数开始执行时,js会为该函数创建执行上下文并压入调用栈中。与此同时,还有一个记录当前执行状态的指针(称为 ESP),指向调用栈中 showName 函数的执行上下文,表示当前正在执行 showName 函数。 当函数执行完成后, JavaScript 引擎会向下移动ESP指针,表示此函数已经执行完成。此时该函数的上下文虽然还在内存中,但是已经是无效内存,再次执行其他函数时,这块内容会被直接覆盖掉,用来存放另外一个函数的执行上下文。
所以说,当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。
堆回收
通过上面的介绍,我们知道,当所有函数执行完成后,ESP指针应该是指向全局上下文,但是在之前的函数中创建的对象,仍然保存在堆内存中,并没有被销毁掉,所以此时,就需要js的垃圾回收器来回收了。
接下来我们就来通过 Chrome 的 JavaScript 引擎 V8 来分析下堆中的垃圾数据是如何回收的。
代际假说(The Generational Hypothesis)”
代际假说的两个特点:

  • 大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
  • 不被回收的对象可能会存活更长时间。

在 V8 中会把堆分为新生代老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。


新生代的容量更小,老生代的容量就大得多了,对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
在64位系统中,通常新生代的最大容量大小默认为32MB,老生代最大容量大小为1.4G。
在32位的系统中为通常新生代的最大容量大小默认为16MB,老生代最大容量大小为0.7G。

  • 副垃圾回收器,主要负责新生代的垃圾回收。
  • 主垃圾回收器,主要负责老生代的垃圾回收。

新生代
副垃圾回收器主要负责新生区的垃圾回收。而通常情况下,大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的。新生代中用 Scavenge 算法来处理
Scavenge
这个算法的基本思想是把某个空间里的活跃对象复制到其他空间,把原来的空间全部清空,这就相当于是把活跃的对象从一个空间搬到新的空间。因为这种复制具有方向性,所以我们把原空间称为 From 空间,把新的目标空间称为 To 空间。分配新的对象都是在 From 空间中,所以 From 空间也被称为分配空间(Allocation Space),而 To 空间则相应地被称为幸存者空间(Survivor Sapce)。


新加入的对象都会存放到From空间,当From空间快被写满时,就需要执行一次垃圾清理操作。
在垃圾回收过程中,首先要对From空间中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到To,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。
完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
由于新生代中采用的 Scavenge 算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略。


对象晋升策略

  • 经历过一次 Minor GC(新生代垃圾回收)后依然存活的对象。V8 的新生代区域采用 Scavenge 算法进行垃圾回收,这种算法通过将新生代区域分为两个等大的空间(一个用于分配对象,另一个空闲),在进行垃圾回收时,会检查使用中的空间里的对象是否还存活,存活的对象会被复制到另一块空闲的空间中(即晋升),非存活对象则被释放。
  • 大对象直接分配到老生代。为了避免在新生代中因频繁复制大对象而造成的资源浪费,V8 在分配对象时会检测对象的大小。如果对象超过预设的大小阈值,它会直接被分配到老生代区域中。
  • To space(新生代垃圾回收后的目标空间)占用量超过一定比例的对象。在进行垃圾回收的复制算法过程中,如果存活的对象过多导致“to space”(存放新复制对象的空间)占用量超过一定比例(如25%),那些对象也会被晋升到老生代来避免下一次垃圾回收时的过度复制。

老生代
主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生代中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长
标记 - 清除(Mark-Sweep)
首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
接下来就是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程完全不同,你可以理解这个过程是清除掉红色标记数据的过程,可参考下图大致理解下其清除过程:


标记 - 整理(Mark-Sweep)
标记清除过程上面的标记过程和清除过程就是标记 - 清除算法,不过对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记 - 整理(Mark-Compact),这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。你可以参考下图:


全停顿
现在你知道了 V8 是使用副垃圾回收器和主垃圾回收器处理垃圾回收的,不过由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
比如堆中的数据有 1.5GB,V8 实现一次完整的垃圾回收需要 1 秒以上的时间,这也是由于垃圾回收而引起 JavaScript 线程暂停执行的时间,若是这样的时间花销,那么应用的性能和响应能力都会直线下降。主垃圾回收器执行一次完整的垃圾回收流程如下图所示:

  1. 增量标记(Incremental Marking)算法

增量标记(Incremental Marking)
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。如下图所示:


增量标记使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。



文章转自:https://juejin.cn/post/7374608035837132863

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值