又是你小子,你到底会不会垃圾回收机制啊


theme: nico
highlight: atom-one-dark

开篇

  • 你是否曾为不会回答好垃圾回收而烦恼,或者因为答不好垃圾回收因为面试挂掉。
  • 你是否只会编写 JavaScript 代码,而不知道什么是内存,为什么内存这么重要?
  • 相信我,阅读这篇文章,你将收获以下知识点:
  1. 什么是内存?
  2. 内存在操作系统中的表现?
  3. V8中的内存管理;
  4. 什么是垃圾回收机制?
  5. 垃圾是怎么产生的?
  6. 为什么要进行垃圾回收?
  7. 垃圾回收是怎样进行的?
  8. V8引擎对垃圾回收做了怎么样的优化?

操作系统中的存储结构

  • CPU 只能从内存中加载指令,因此执行程序必须位于内存。通用计算机运行的大多数程序通常位于可读写内存,称为内存,也称为 随机访问内存
  • 内存由一个很大的字节数组来组成,每个字节都有各自的地址,CPU 根据程序计数器的值从内存中提取指令,这些指令可能引起对特定内存地址的额外加载与存储。
  • 在理想状态下,程序和数据都应永久驻留在内存中。由于以下两个原因,这是不可能的:
  1. 内存通常太少,不能永久保存所有需要的内存和数据;
  2. 内存是 易失性 的存储设备,停电时就会失去所有内容;
  • 最为常用的外存设备为磁盘和硬盘,它能存储程序和数据。大多数程序(例如谷歌浏览器的安装文件)都保存在硬盘上,当要执行的时候才加载到内存,操作系统还会为其开辟一个线程以运行程序。如下图:

image.png

  • 在上图中,操作系统会为每个应用程序开辟一个线程,并且每个应用程序都占据着一定的内存空间。
  • 进程又能创建线程,并且每个进程最少拥有一个线程,称为主线程,如下图:

image.png

  • 一个谷歌浏览器拥有多个线程,用于运行每一个浏览器页面。

image.png

  • 根据速度和价格,各种不同的存储可以按层次来分类,如上图所示。层次越高,价格越贵,速度越快。
  • 从高到低,每个层次的价格通常会降低,而访问时间通常会增加。
  • 寄存器和存储器的区别:
  1. 存储器可以存放指令和数据,并能由中央处理器(CPU)直接随机访问;
  2. 存储器可将寄存器内的数据执行算术及逻辑运算。存于寄存器内的地址可用来指向内存的某个位置,即寻址,也可以用来读写数据到电脑周边设备;
  3. 寄存器的速度比主存储器的速度要快很多,因为是有限的空间读取存储有限的数据,存存器中的数据必须放入寄存器才能够进行操作。简单地说:寄存器是操作数据的地方,存储器是存放数据的地方;

内存管理

  • 在前面的内容中我们对内存进行了介绍,那么接下来我们认识一下内存管理。
  • 不管什么样的编程语言,在代码执行过程中都是需要给它分配内存的,不同的是某些编程语言需要我们自己手动的管理内存,某些编程语言会可以自动帮助我们管理内存。
  • 像 C 语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()free()
  • 相反,JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让 JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。

内存生命周期

  • 在JavaScript中的内存管理中生命周期分为三个阶段,当然其他语言也是一样的:
  1. 分配内存:当我们申请变量、函数、对象的时候,系统会自动为它们分配内存;
  2. 内存使用:即读写内存,也就是使用变量、函数等;
  3. 内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存;
  • 通过下面的代码我们来简单分析一下整个内存管理的生命周期:
// 分配内存
const foo = {
  name: "moment",
  age: "18",
};

// 使用内存
foo.name = "xun";
console.log(foo);

// 内存回收
foo = null;
  • 那么怎么样的内存会被称为垃圾呢,有以下两点:
  1. 对象不再被引用时是垃圾;
  2. 对象不能从根上访问时是垃圾,其中 JavaScript 中的根可以理解为全局对象;

内存分配

  • 在JavaScript中,不同的数据类型分配不同的内存,而存放这些数据的内存又可以分为两部分:栈内存堆内存
  • JavaScript 中有7种原始类型,它们分别是 空值(null)、未定义(undefined)、布尔值(boolean)、数字(number)、字符串(string)、 任意精度整数(Bigint)、符号(symbol),这些类型都会被存放到栈内存中。
  • JavaScript中的引用类型,比如 ObjectArray,它们是存在堆内存的,JavaScript不允许直接操作堆内存,我们操作对象时,操作的实际是对象的引用,而不是实际对象,这就相当于 C语言 中的指针,这个指针指向了堆里面的实际的对象。函数也是引用类型,当我们定义一个函数时,会在堆内存中开辟一块内存空间,将函数体代码以字符串的形式存进去。然后将这块内存的地址赋值给函数名,函数名和引用地址会存在栈上。

image.png

  • 栈存储无论分配新的空间还是释放空间(压栈和退栈)都很简单,访问栈里的变量也快速,但其缺点是每次压栈的空间大小是固定的,因此里面的变量的数目及其数据结构大小也是固定的。
  • 压栈和退栈是随着函数调用同步进行的,当函数结构后期栈空间会被立即释放,里面的变量的数据无法保留。如果要保留,让函数外面的继续,比如闭包,必须将其存入堆内存中,闭包中的变量也是存在堆内存中,堆存储的特点是不会随函数的结束而自动让数据消失。
  • 堆在分配和释放空间时要做相当多的工作,比如分配时寻找合适大小的空间,对不用的空间做垃圾扫描和垃圾回收,甚至要将碎片化的空闲空间整合在一起。此外访问堆里的数据也要比栈更慢。所有这些都让堆储存的运行代价很高,影响性能。

什么是GC

  • GCGarbage Collection,程序过程中会产生很多 垃圾,这些垃圾是程序不再使用的内存或者一些不可达的对象,而 GC 就是负责回收垃圾的,找到内存中的垃圾、并释放和回收空间。

d0c24eaab16ff865296c1fed8ba5b94.jpg

  • CC++ 等语言中,跟踪内存使用对开发者来说是个很大的负担,也是很多问题的来源。JavaScript 为开发者卸下了这个负担,通过自动内存管理实现内存分配和闲置资源回收。
  • 整个过程是周期性的,即垃圾回收程序每隔一定时间或者说代码执行过程中某个预定阶段的收集时间就会自动运行。
  • 垃圾回收过程是一个近似且不完美的方案,因为某块内存垃圾回收是否还有用,属于 不可判定的 问题,意味着靠算法是解决不了的。
  • 在浏览器的发展历史上,用到过两种主要的标记策略:标记清理引用计数

引用计数

  • 引用计算的核心思想是对每个值都记录它被引用的次数。声明变量并给他赋一个引用值时,这个值的应用数为1。
  • 如果同一个值又被赋给另一个变 量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一 个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序 下次运行的时候就会释放引用数为 0 的值的内存。
  • 引用计数有一个严重的问题,就是循环引用,所谓的循环引用,就是对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A,比如:
function foo() {
  const A = {};
  const B = {};

  A.foo = B;
  B.foo = A;
}
  • 在这个例子中,AB 通过各自的属性相互引用,意味着它们的引用数都是2。在 标记清理策略下,这不是问题,因为在函数结束后,这两个对象都不在作用域中。而在引用计数策略下,AB 在函数结束后还会存在,因为它们的引用数永远不会变成0。如果函数被多次调用,则会导致大量内存永远不会被释放。
  • 引用计数的优势:
  1. 可即刻回收垃圾,当被引用数值为0时,对象在变成垃圾的时候就立刻被回收。
  2. 因为是即时回收,那么‘程序’不会暂停去单独使用很长一段时间的GC,那么最大暂停时间很短。
  • 引用计数的缺点:
  1. 时间开销大,因为引用计数算法需要维护引用数,一旦发现引用数发生改变需要立即对引用数进行修改;
  2. 最大的缺点还是无法解决循环引用的问题;
  • 看一个例子,很鲜明的表示了引用计数存在的缺点了:

function foo() {
  const A = {};
  const B = {};

  A.foo = B;
  B.foo = A;

  return "hi";
}

foo();
  • foo在执行完成之后理应回收foo作用域里面的内存空间,但是因为 A 里面有一个属性引用 B,导致B的引用次数始终为1,B也是如此,而又非专门当做闭包来使用,所以这里就应该使AB被销毁。因为算法是将引用次数为0的对象销毁,此处都不为0,导致GC不会回收他们,那么这就是内存泄漏问题。
  • 一直手动解决的办法就是把变量设置为 null 实际上会切断变量与其之前引用值之间的关系。当下次垃圾回收程序运行时, 这些值就会被删除,内存也会被回收。

标记清理

  • 在JavaScript中,最常用的是垃圾回收策略是 标记清理
  • 。当变量进入上下文,比如在函数 内部声明一个变量时,这个变量会被加上存在于上下文中的标记。而在上下文中的变量,逻辑上讲,永远不应该释放它们的内存,因为只要上下文中的代码在运行,就有可能用到它们。当变量离开上下文时, 也会被加上离开上下文的标记。
  • 标记清理分为两个阶段:
  1. 标记阶段: 把所有活动对象做上标记;
  2. 清除阶段: 把没有标记或者说非活动对象销毁;
  • 给变量加标记的方式有很多种。比如,当变量进入上下文时,反转某一位;或者可以维护在 上下文 中和 不在上下文中 两个变量列表,可以把变量从一个列表转移到另一个列表。标记过程的实现并不重要,关键是策略。
  • 引擎在执行 标记清除算法 时,需要从出发点去遍历内存中所有对象去打标志,而这个出发点就是 根对象,在浏览器中你可以理解为 windows,整个标记清除算法大致过程就像下面这样:
  1. 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0;
  2. 然后从 根对象 开始深度遍历,把不是垃圾的节点改成1;
  3. 清除所有标记为0的垃圾,销毁并回收它们所占用的内存空间;
  4. 最后把内存中的所有对象标志修改为0,等待下一轮垃圾回收;
  • 如下图,标记清除算法会把最下面的那两个小垃圾清除掉:

image.png

  • 标记清除算法的优点:
  1. 实现简单,打标记也就是打或者不打两种可能,所以就一位二进制位就可以表示;
  2. 解决了循环引用的问题;
  • 标记清除算法的缺点:
  1. 内存碎片化(内存零零散散的存放,造成资源浪费);
  2. 再分配时遍次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端;
  3. 不会立即回收资源;

V8垃圾回收策略

  • V8 的垃圾回收策略主要是基于 分代式垃圾回收策略,其根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同的分代采用不同的垃圾回收算法。
  • 其中,在 JavaScript 中,对象存活周期分为两种情况:
  1. 存活周期很短: 经过一次垃圾回收后,就被释放回收掉;
  2. 存活周期很长: 经过多次垃圾回收后内存仍存在;
  • 对于这些存货周期长的,多次回收都回收不掉的问题,V8 做了分代回收的优化方法,就是 V8 将堆分为两个空间,一个叫新生代,另一个叫老生代,新生代是存放存活周期短对象的地方,老生代是存放存活周期长对象的地方。
  • V8 整个堆存的大小就等于新生代加上老生代的内存,也就是分代内存,如下图:

image.png

  • 新生代通常只有 1-8M 的容量,而老生代的容量就大很多了。对于这两块区域,V8 分别使用了不同的垃圾回收器和不同的回收算法,以便更高效地实施垃圾回收。
  • 在默认情况下,如果一直分配内存,在64位系统和32位系统下分别只能使用约1.4GB和约0.7GB的大小

新生代垃圾回收

  • 按机器数位不同, 在64位系统和32位系统上分别为16MB8MB,所以新生代内存的最大值在64位系统32位系统分别为32MB16MB
  • 在分代的基础上,新生代中的对象主要通过 Scavenge 算法进行垃圾回收。在 Scavenge 算法的具体中,主要采用了 Chenney 算法:
  • Cheney 算法是一种采用复制的方式实现垃圾回收算法。它将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于空闲状态。
  • 处于使用状态的 semispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。
  • 当我们分配对象时,先是在 From 空间中进行分配,当进行垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,而非存活对象占用的空间就会被释放。
  • Scavenge 算法的缺点是只能从堆内存中的一半,这是由划分空间和复制机制所决定的。但是由于 Scanvenge 只复制存活的对象,并且对于生命周期的场景存活对象只占少部分,所以它在时间效率上有优异的表现,由于 Scanvenge 是典型的牺牲空间获取时间的算法,所以无法大规模地应用所有的垃圾回收中,但是 Scanvenge 非常适合应用在新生代中,因为新生代中对象的生命周期较短,刚刚好适合这个算法。
  • V8 的堆内存示意图如下图所示:

image.png

  • 在整个 V8 内存堆中,实际使用的堆内存新生代中的两个 semispace 空间大小和老生代所用内存大小之和。
  • 当一个对象经过多次复制依然存活时,它将会被认为是生命周期较长的对象。这种较长生命周期对象周期的对象随后会被一道老生代中,采用新的算法进行管理。对象从新生代中移动到老生代中的过程称为晋升。
  • 在单纯的 Scavenge 过程中,From 空间中的存活对象会被复制到 To 空间中去,然后对 From 空间和 To 空间进行角色对换(又称翻转),这个角色对换简单的说就是把原来的使用区全部清空使其变成空闲区,把原来的空闲区变成使用区。但在分代式垃圾回收的前提下,From 空间中的存活对象在复制到 To 空间之前需要进行检查。在一定条件下,需要将存活周期长的对象移动到老生代中,也就是完成对象晋升。
  • 对象晋升的条件主要有两个,一个是对象是否经历过 Scavenge 回收,一个是 To 空间的内存占用比超过限制。
  • 在默认情况下,V8的对象分配主要集中在From 空间中。对象从 From 空间中复制到 To 空间时,会检查它的内存地址来判断这个对象是否经历过一次 Scavenge 回收。如果经历过了,会检查它的内存地址来判断这个对象是否经历过一次 Scavenge 回收。
  • 如果已经经历过了,会将该对象从 From 空间复制到老生代空间中,如果没有,则复制到 To 空间中。这个晋升流程如下图所示:

image.png

02.svg

  • 上图来源于 V8官网
  • 另一个判断条件是 To 空间的内存占用比。当要从 From 空间复制一个对象到 To 空间时,如果 To 空间已经使用了超过 25%,则这个对象直接晋升到老生代空间中。设置 25% 这个限制值的原因是当这次 Scavenge 回收完成后,这个 Scavenge 回收完成后,这个 To 空间将变成 From 空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。这个晋升的判断示意图如下:

image.png

  • 对象晋升后,将会在老生带空间作为存活周期较长的对象来对待,接收新的回收算法来处理。

老生代垃圾回收

  • 新生代空间里的对象,经过大战三百回合,终于杀到了决赛圈,成功晋升到了老生代空间里,这些对象是经过多次垃圾回收过程但是没有被收走的,在这个决赛圈里,在这个决赛圈里,因为它们知道 Scavenge 回收算法有两个问题:
  1. 存活对象较多,复制存活对象的效率将会很低;
  2. 另一个问题依然是浪费一半空间的问题;
  • 对它们不管用,于是在新生代采用了 Mark-Sweep(标记清除)Mark-compact(标记整理)
  • Mark-Sweep 分为标记清除两个阶段。与 Scavenge 相比,Mark-Sweep 并不将内存空间划分为两半,所以不存在浪费一半空间的行为。
  • Scavenge 复制或者的对象不同,Mark-Sweep 在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的阶段。
  • Mark-Sweep 只清除死亡对象,而 Scavenge 只复制活着的对象。
  • 活对象在新生代中只占小部分,死对象在老生代中只占小部分,这是两者垃圾回收能高效处理的原因。如下图为 Mark-Sweep 标记后的示意图,红色部分标记为死亡的对象:

image.png

  • Mark-Sweep 最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。
  • 为了解决这个问题,Mark-Compact 算法被提出来了,Mark-Compact 是标记整理的意思,是在 Mark-Sweep的基础上演变而来的。它们的差别在于对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成之后,直接清理边界外的内存。

全停顿

  • 因为垃圾回收也要用到 JavaScript 引擎,JavaScript 代码的运行也要用到 JavaScript 引擎,那如果这两者同时进行了,发生冲突了怎么解决?
  • 为了避免出现 JavaScript 应用逻辑与垃圾回收期看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑停下来,得执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为 全停顿(stop-the-world)
  • V8 的分代式垃圾回收中,新生代中存活对象通常较少,所以即便它是全停顿的影响也不大。但 V8 的老生代配置得较大,且存活对象较多,全堆垃圾回收的标记、清理、整理等动作造成的停顿就会比较明显。
  • 为了降低全堆垃圾回收带来的停顿时间,V8 先从标记阶段入手,原本一次停顿完成的动作改为增量标记,也就是拆分为许多小 步进,每做完一 步进 就让 JavaScript 引用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成,如下图增量标记图所示:

image.png

Orinoco

  • OrinocoV8 垃圾回收器项目的代号,它利用最新的和最好的垃圾回收技术来降低主线程挂起的时间,比如:并行垃圾回收,增量垃圾回收,延迟清理并发垃圾回收。其主要目的是进一步利用多核性能降低每次停顿的时间,提升用户体验。

并行垃圾回收

  • 并行是主线程和协助线程同时执行同样的工作,但是这仍然是一种 stop-the-world 的垃圾回收方式,但是垃圾回收所耗费的时间等于总时间除以参与的线程数量。这是这几种方式中最简单的一种。

image.png

增量垃圾回收

  • 增量式垃圾回收是主线程间歇性的去做少量的垃圾回收的方式。不会在增量式垃圾回收的时候执行整个垃圾回收的过程,只是整个垃圾回收过程中的一小部分工作。这就相当于你平时炒菜,不是一直等着菜炒熟再去做另外一件事,而是在炒菜的过程中准备下一道菜的准备工作。
  • 但是在 JavaScript 中做这样的工作是极其困难的,因为 JavaScript 也在做增量式垃圾回收的时候同时执行,这意味着堆的状态已经发生了变化,这有可能会导致之前的增量回收工作完全无效。但这仍然是解决问题的好方法,通过 JavaScript 间歇性执行,同时也间歇性进行垃圾回收,两者交替执行,从而提高效率,如下图:

image.png

并发垃圾回收

  • 并发是主线程一直执行 JavaScript 应用程序,而辅助线程在后台完全的执行垃圾回收。
  • 这是这些技术中最难的一种,JavaScript 堆里面的内容随时都有可能发生变化,从而使我们之前所做的工作无效。
  • 除此之外,还存在读/写竞争,主线程和辅助线程极有可能在同一时间去更改同一个对象。这种方式的优势也很明显,主线程不会被挂起,JavaScript 可以自由地执行,虽然会有一些与辅助线程的一些同步操作而导致的开销,但是它们都很小:

image.png

并行是同一个时间点同时发生的,例如你一边吃饭一边学习,而并发是同一个时间段同时发生的,你早上学HTML,下午学Css,晚上学JavaScript,不停的学,这称为并发。

图中的并行和并发的图似乎写反了,不知道是不是真的如我想的那样,我也不知道……

延迟清理(Lazy Sweeping)

  • 增量标记只是对活动对象和非活动对象进行标记,对于真正的清理释放内存,V8 则采用的是 延迟清理
  • 之所以采用 延迟清理,是因为当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,所以没必要立即清理内存或者只清理部分垃圾,而不清理全部。
  • 增量标记和延迟青丽 的出现,大大改变了 全停顿 的现象。但是增量标记是标记一点,JavaScript执行一段,那如果你刚标记完一个活动对象,js代码就把该对象设置为非活动对象或者反过来,这就有可能引起对象引用改变,标记错误的现象。这就需要使用 写屏障 技术来记录这些引用关系的变化。

三色标记法

  • 老生代是采用标记清理算法,在没有采用增量算法之前,单纯使用黑色和白色来标记数据就可以了,其标记流程即在一次执行完整的标记前,垃圾回收会将所有数据设置为白色,然后从根开始深度遍历,将所有能访问到的数据标记为黑色,标记为黑色的数据对象就是活动对象,剩余的白色数据对象也就是待清理的垃圾对象。
  • 如果采用黑白的标记策略,那么垃圾回收器执行了一段增量回收后,暂停后启用主线程去执行了 JavaScript 应用程序,随后垃圾回收器再次被启动,这时候内存黑白色都有,我们便不能得知下一步走到哪里了。
  • 为了解决这个问题,V8 团队采用了一种特殊方式,即 三色标记法,也就是使用每个对象的两个标记位和一个标记工作表来实现标记。
  • 其中两个标记位编码三种颜色:白色(00),灰色(10)和黑色(11)。
  • 最初所有的对象都是白色,意味着收集器还没有发现他们。当收集器发现一个对象时,将其标记为灰色并推入到标记工作表中。当收集器从标记工作表中弹出对象并访问他的所有字段时,灰色就会变成黑色。这种方案被称做 三色标记法。当没有灰色对象时,标记结束。所有剩余的白色对象无法达到,可以被安全的回收。

image.png

image.png

image.png

  • 采用 三色标记法 后我们在恢复执行时就好办多了,可以直接通过当前内存中没有灰色节点来判断整个标记是否完成,如没有灰色节点,直接进入清理阶段,如还有灰色标记,恢复时直接从灰色的节点开始执行即可。
  • 这个方法可以很好的配合 增量回收 进行暂停恢复的一些操作,从而减少 全停顿 的时间。

如果标记好的数据若是被主线程修改了,那么该如何处理呢?

  • V8 使用了写屏障机制来实现,这个机制也不难理解,简单来讲就是强制让黑色的对象不能直接指向白色的对象。
  • 将新引入的对象从初始的白色直接变为灰色,那么 标记工作表 就没有空,那么就继续执行标记的过程,保证了正确的标记数据。

V8当前的垃圾回收机制

副垃圾回收器

  • V8 在当前新生代垃圾回收中使用并行清理,每个协程会将所有活动对象都移动到 To 空间。在每一次尝试将活动对象移动到 To 空间的时候都必须原子化的读和写以及比较和交换操作。不同的协助线程都有可能通过不同的路径找到相同的对象,并尝试将这个对象移动到 T0 空间。
  • 无论哪个协助线程成功移动对象到 To 空间,都必须更新这个对象的指针,并且去维护移动这个活动对象所留下的转发地址。以便于其他协助线程可以找到该活动对象更新后的指针。
  • 为了快速的给幸存下来的活动对象分配内存,清理任务使用线程局部分配缓冲区。

image.png

主垃圾回收器

  • V8 中的主垃圾回收器主要使用并发标记,一旦堆的动态分配接近极限的时候,将启动并发标记任务。
  • 每个辅助线程都会去追踪每个标记到的对象的指针以及对这个对象的引用,而在JavaScript代码执行时候,并发标记也在后台的辅助进程中进行,写入屏障 技术会在辅助线程在进行并发标记的时候会一直追踪每一个 JavaScript 对象的新引用。

image.png

  • 当并发标记完成或者动态分配到达极限的时候,主线程会执行最终的快速标记步骤;在这个阶段主线程会被暂停,这段时间也就是主垃圾回收器执行的所有时间。
  • 在这个阶段主线程会再一次的扫描根集以确保所有的对象都完成了标记;然后辅助线程就会去做更新指针和整理内存的工作。
  • 并非所有的内存页都会被整理,之前提到的加入到空闲列表的内存页就不会被整理。在暂停的时候主线程会启动并发清理的任务,这些任务都是并发执行的,并不会影响并行内存页的整理工作和 JavaScript 的执行。

参考文献

结尾

  • 文章内容大多来自官网或者一些其他的文章,你们就当做是一个笔记吧。
  • 如果有疑问的欢迎评论区留言,如果有错误的欢迎批评指出。
  • 如果觉得不错,那就点个赞呗。
© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容