V8 如何进行垃圾回收?

简介: V8 如何进行垃圾回收?

image.png


前言

想要了解 V8 引擎如何实现垃圾回收的,那么前提是你得知道 JavaScript 中的数据是如何存储在内存中,那么下面就一起来了解数据是如何在内存中的存储的。

数据是如何存储的?

内存空间

JavaScript 的执行过程中, 主要有三种类型内存空间:

  • 代码空间:主要是存储可执行代码的,这里不是主要介绍的内容
  • 栈空间
  • 堆空间

首先来观察如下 代码片段一 的输出结果:

function foo(){
    var a = 1
    var b = a
    a = 2
    console.log(a)
    console.log(b)
}
foo()
复制代码

显然这对你来说应该没什么难度,输出结果为:2 1

接下来在看 代码片段二 代码的输出结果:

function foo(){
    var a = { msg: "hello world" }
    var b = a
    a.msg = "hello juejin" 
    console.log(a)
    console.log(b)
}
foo()
复制代码

这是不是也没什么难度,输出结果为:hello juejin

也许你知道输出的结果是什么,但是你能说清楚这是为什么吗?如果你不能有一个很好的回答思路和方向,那就继续往下看吧。

栈空间

栈空间其实就是经常提到的调用栈,是用来存储执行上下文的,它一般是用来存储原始类型数据.

下面通过 代码片段一 的例子来具象化这个内容,如下:

function foo(){
    var a = 1
    var b = a
    a = 2 // 执行到此处
    console.log(a)
    console.log(b)
}
foo()
复制代码

当代码执行到 a = 2 处时,对应的 调用栈 内容可以具象化为:

image.png

从图中可以看出来,原始类型数据是存放在栈空间的,当 JavaScript 需要访问该数据的时候,可以直接访问到栈中值。

堆空间

堆空间内存比栈空间更大,一般用来存储引用类型数据,栈空间中一般存储的是堆内存对应的引用地址.

下面通过 代码片段二 的例子来具象化这个内容,如下:

function foo(){
    var a = { msg: "hello world" }
    var b = a // 执行到此处
    console.log(a)
    console.log(b)
}
foo()
复制代码

当代码执行到 var b = a 处时,对应的 调用栈 内容可以具象化为:

image.png

从图中可以看出来,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址,当 JavaScript 需要访问该数据的时候,是通过栈中的引用地址来访问的。

为什么要分 “堆空间” 和 “栈空间”?全部都使用栈空间存储可不可以?

全部都使用栈空间存储的方案是不可行的,因为 JavaScript 引擎需要用 来维护程序执行期间上下文的状态,如果栈空间设置太大,所有的数据都存放在栈空间里面,那么会影响到 上下文切换的效率,这又会影响到 整个程序的执行效率

image.png

所以通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间。

垃圾数据是如何自动回收的?

通常情况下,垃圾数据回收分为两种策略:

  • 手动回收:如 C/C++ 就是使用手动回收策略,何时分配内存、何时销毁内存都是由代码控制的
  • 自动回收:如 JavaScript、Java、Python 等语言,产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放

如何回收调用栈中的数据?

仍然通过示例代码的执行流程具象化回收机制:

function foo(){
    var a = 1
    var b = { msg: "hello world" }
    function bar(){
      var c = 2
      var d = { msg: "hello juejin" } // 执行到此处
    }
    bar()
}
foo()
复制代码

当代码执行到 var d = { msg: "hello juejin" } 处时,其调用栈和堆空间状态图如下所示:

image.png

除了上图中显示内容外,还有一个用于记录当前执行状态的指针(称为 ESP),指向调用栈中 bar 函数的执行上下文,表示当前正在执行 bar 函数.

当 bar 函数执行完成后,ESP 指针就会从 bar 函数执行上下文下移到 foo 函数执行上下文,如下图所示:

image.png

从上图可知,当 bar 函数执行结束之后,ESP 向下移动到 foo 函数的执行上下文中,上面 bar 的执行上下文虽然保存在栈内存(无效内存)。比如当 foo 函数再次调用另外一个函数时,这块无效内存会被直接覆盖掉,用来存放另外一个函数的执行上下文。

因此,当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。

如何回收堆中的数据?

当上面那段代码的 foo 函数执行结束之后,ESP 应该是指向 全局执行上下文,那意味着,虽然 bar 函数和 foo 函数的执行上下文已经处于无效状态,但保存在堆中的两个对象依然占用着空间,如下图所示:

image.png

从上图可知,1001 和 1002 这两块内存依然被占用着,而如果要回收堆中的垃圾数据,那就需要用到 JavaScript 中的 垃圾回收器

什么是代际假说?

代际假说是垃圾回收领域中一个重要的术语,垃圾回收的策略都是建立在它的基础上。

代际假说(The Generational Hypothesis)有两个特点:

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

什么是分代收集?

V8 中会把堆分为两个区域:

  • 新生代:新生代中存放的是 生存时间短 的对象,通常只支持 1~8M 的容量
  • 老生代:老生代中存放的 生存时间久 的对象,支持的容量就比新生代大很多

对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收:

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

垃圾回收器的工作流程

现在我们知道了 V8 中会分为 新生代老生代 两块区域,也知道它们对应着 副垃圾回收器主垃圾回收器,看起来垃圾回收器有 主副 之分,但不论什么类型的垃圾回收器,它们都有一套共同的执行流程:

  • 第一步标记 空间中 活动对象非活动对象

活动对象 指还在使用的对象,非活动对象 指可进行垃圾回收的对象

  • 第二步回收非活动对象所占据的内存
  • 即在完成所有标记后,统一清理内存中所有被标记为可回收的对象
  • 第三步 是做 内存整理
  • 频繁回收对象后,内存中就会存在大量不连续空间,即 内存碎片
  • 内存中出现大量的 内存碎片,可能会导致被后期分配内存时出现 内存不足 的情况
  • 注意 副垃圾回收器 不会产生 内存碎片

副垃圾回收器

副垃圾回收器 主要负责 新生区 的垃圾回收。而通常情况下,大多数小的对象都会被分配到 新生区,这个区域虽然不大,但是垃圾回收还是比较频繁的。

新生代中用 Scavenge 算法来处理,也就是把新生代空间对半划分为两个区域,一半是对象区域一半是空闲区域

image.png

新加入的对象都会存放到 对象区域,当对象区域 快被写满 时,就需要执行一次 垃圾清理 操作,回收过程仍然是上面提到的三步,以及外加一些策略:

  • 对象区域 中的垃圾做 标记
  • 标记完成之后,就进入 垃圾清理阶段副垃圾回收器 会把这些 存活 的对象 复制空闲区域
  • 由于上一步的操作,使得不会有 内存碎片 产生,也没必要进行 内存整理
  • 完成复制后,对象区域空闲区域 进行角色 翻转,即原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域

为什么新生代的空间会被设置得比较小?

由于新生代中采用的 Scavenge 算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。

复制操作需要时间成本,如果新生区空间设置得太大,会导致每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小

也正是因为新生区的空间不大,所以 很容易 被存活的对象 装满 整个区域。为了解决这个问题,JavaScript 引擎 采用了 对象晋升策略,即经过 两次垃圾回收 依然存活的对象,会被移动到 老生区 中。

主垃圾回收器

主垃圾回收器 主要负责 老生区 中的垃圾回收

老生区中的对象有两个特点:

  • 一是对象占用空间大,即一些大的对象会直接被分配到老生区
  • 二是对象存活时间长,即新生区中经过两次垃圾回收,然后存活的对象,即 晋升对象

标记 - 清除(Mark-Sweep)算法

由于以上特点,主垃圾回收器不适用于 Scavenge 算法,它采用 标记 - 清除(Mark-Sweep) 算法进行垃圾回收:

  • 标记过程阶段
  • 从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达 的元素称为 活动对象没有到达 的元素就为 垃圾数据
  • 垃圾的清除过程
  • 它和 副垃圾回收器 的垃圾清除过程 完全不同,可以将这个过程具象化为是清除掉红色标记数据的过程
  • image.png

标记 - 整理(Mark-Compact)算法

从上图可知,标记 - 清除算法 会产生 内存碎片,而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了 标记 - 整理(Mark-Compact)算法

这个标记过程仍然与 标记 - 清除算法 是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有 存活对象 都向 一端移动,然后直接清理掉端边界以外的内存,如图:

image.png

全停顿

V8 是使用 主副垃圾回收器 来进行垃圾回收,不过由于 JavaScript 是运行在 主线程 之上的,一旦执行 垃圾回收算法,最终都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行,即 全停顿(Stop-The-World)

image.png

增量标记(Incremental Marking)算法

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

image.png

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

总结

关于内存和垃圾回收的内容,大多属于是概念性的,在前端进阶上又是无法避免的部分,概念虽然比较多,但如果我们能将这些概念具象化,那么也就能更容易掌握它们。


目录
相关文章
|
7月前
|
存储 算法 JavaScript
V8如何进行垃圾回收的
V8如何进行垃圾回收的
20 0
|
12月前
|
存储 Web App开发 监控
Js中的垃圾回收及V8引擎的优化
Js中的垃圾回收及V8引擎的优化
244 0
|
缓存 JavaScript 前端开发
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
288 0
图解 Google V8 # 22 :关于内存泄漏、内存膨胀、频繁垃圾回收的解决策略(完结篇)
|
Web App开发 JavaScript 前端开发
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
112 0
图解 Google V8 # 21 :垃圾回收(二):V8是如何优化垃圾回收器执行效率的?
|
算法 JavaScript Java
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
98 0
图解 Google V8 # 20 :垃圾回收(一):V8的两个垃圾回收器是如何工作的?
ADI
|
算法 Java
[记录]简单聊聊 v8 垃圾回收
[记录]简单聊聊 v8 垃圾回收
ADI
77 0
|
算法 Java
|
19天前
|
算法 Java
JVM GC和常见垃圾回收算法
JVM GC和常见垃圾回收算法
54 0
|
19天前
|
Java Go
Golang底层原理剖析之垃圾回收GC(二)
Golang底层原理剖析之垃圾回收GC(二)
54 0
|
19天前
|
存储 缓存 算法
JVM(四):GC垃圾回收算法
JVM(四):GC垃圾回收算法