Javascript之V8内存和垃圾回收讲解

发布时间 2023-03-26 13:51:12作者: 上善若泪

1 Javascript内存

1.1 Javascript引擎

常见JavaScript引擎有V8JavaScriptCoreTraceMonkeyJScriptJavaScript在不同引擎中的内存模型实现不同,其中V8引擎开源且市占率远高于其它引擎,因此本文将围绕V8进行讲解。

在学习之前,要明白为什么要学习内存模型:

  • 编写合理的代码,避免内存泄露;
  • 理解按值传递和按引用传递到底是什么,引用类型和值类型是什么;
  • 理解深拷贝和浅拷贝;
  • 理解闭包

1.2 V8内存模型

在这里插入图片描述
从图中可以看到,JS虚拟内存空间分为两大部分,其中堆空间又被进一步划分为多个分区,这么划分堆空间与垃圾回收算法有关,后面会作说明解。目前我们只需要记住虚拟内存空间包含两大部分。

1.2.1 栈

栈特点:

  • 栈是一个数组结构,遵循先入后出的原则
  • 栈是一个 临时变存储空间,在javascript中主要存储局部变量函数调用
  • 存放有固定大小的数据类型
  • 基础数据类型的变量都是直接存储在栈中(JS中基础数据类型有: Number、 String、 Null、 Undefined 、Boolean、Symbol,)。复杂类型数据会将对象的引用(实际存储的指针地址)存储在栈中,数据本身存储在堆中
  • 主要存放:Undefined、不是new出来的布尔、数字和字符串,它们都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间
  • 对于调用时,js解释器都会在栈中创建一个调用栈(call stack)来存储函数的调用流程顺序。然后把该函数添加进调用栈,解释器会为被添加进的函数再创建一个栈帧(Stack Frame),这个栈帧用来保存函数的局部变量以及执行语句)并立即执行。如果正在执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈并执行。直到这个函数执行结束,对应的栈帧也会被立即销毁

1.2.2 堆

堆特点:

  • 堆用于存放大小不固定的数据类型
  • 堆数据结构是一种树状结构。它的存取数据的方式,则与书架与书非常相似。书虽然也整齐的存放在书架上,但是我们只要知道书的名字,就可以很方便的取出我们想要的书,好比在JSON格式的数据中,我们存储的key-value是可以无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书的名字。
    是一种先进先出(FIFO)的数据结构。
  • 主要存放的是引用类型的数据,如对象、数组、函数、Null等(typeof null 为 object),它们是通过拷贝new出来的,这样数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据

堆一般用来存储比较复杂的数据对象。同时也被划分为了各个区:

  • 代码区(code space
    存放预编译代码

  • map区(map space
    存放对象的Map信息。每个Map对象固定大小,为了快速定位,所以将该空间单独出来

  • 大对象区(large object space
    为了避免大对象的拷贝,使用该空间专门存储大对象。包括Code、FixedArray等等

  • 新生代(new space
    大多数对象创建时都会被存储到该区域。主要由两个semispace(半空间)构成,from区域、to区域。内存最大值在64位系统和32位系统上分别为32MB和16MB,在新生代的垃圾回收过程中主要采用了Scavenge算法。
    对象创建时都会被分配在from区域,然后在gc(Garbage Collection)阶段,会将from中还存活的数据复制到to区域,然后交互to区域和from区域。实现一次垃圾回收。如果在两次gc中都还存活的数据就会被移动到老生代区域存储。
    新生代的gc频率很高、速度快、但是空间利用率底,典型的空间换时间的做法

    • 新生代中的数据晋升
      当一个对象在经过多次(两次)复制之后依旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被直接转移到老生代中,这种对象从新生代转移到老生代的过程我们称之为晋升。
      在新生代中的数据晋升需要满足下面两个条件中的任意一个:
      对象是否经历过一次Scavenge算法,如果是则放入老生代数据中
      to区域的剩余的内存占比是否已经低于25%,如果是则放入到老生代中
  • 老生代(old space)
    新生代中多次回收仍然存活的对象会被转移到空间较大的老生代。因为老生代空间较大,如果回收方式仍然采用 Scanvage 算法来频繁复制对象,性能开销会非常大,不适用于这种场景。
    现在在老生代中主要适用标记清除(Mark-Sweep)来进行gc。在以前使用引用计数来进行gc,但是该方式容易造成内存泄漏,主要表现为循环引用的情况下,数据不能被正常的回收,导致内存飙升,造成内存泄漏。
    标记清除(Mark-Sweep)主要分为两个阶段:标记、清除。

1.3 内存生命周期

内存一般有如下生命周期:

  • 内存分配:当我们声明变量、函数、对象的时候,系统会自动分配内存;
  • 内存使用:即读写内存,也就是使用变量、函数等;
  • 内存回收:使用完毕,回收不再使用的内存。

现从栈和堆内存的生命周期来做进一步讲解

1.3.1 栈内对象生命周期

栈内存的生命周期与函数上下文生命周期息息相关,函数上下文生命周期主要分为三个阶段:

  • 内存分配:创建函数执行上下文;
  • 内存使用:执行函数代码;
  • 内存回收:退出函数执行上下文。

每次进行函数调用的时候,都会在栈上分配一段内存空间(这里我们把它叫做栈帧),用于保存当前函数的上下文,当函数执行结束后,就会释放栈栈帧,举个例子:

function pFn(){
    function cFn(){};
    cFn();
};

pFn();

栈内存分配与回收情况如下:
在这里插入图片描述
函数上下文的生命周期讲完了,那么它与栈内对象的生命周期有什么关系呢?我们先来了解一下栈帧分配阶段的流程:

  • 绑定this值;
  • 在栈上创建词法环境;
  • 在栈上创建变量环境。

现在我们无需关注词法环境和变量环境有什么作用,只需要记住两点:
第一,函数上下文信息保存在栈空间,这里我们把它叫做栈帧;
第二,栈帧内存放在当前函数内声明的变量以及外部环境引用。

举个例子:
在这里插入图片描述

函数执行结束后,就会回收当前函数对应的栈帧,当前栈帧中的变量自然会被释放,这就是栈上数据的生命周期。

1.3.2 堆内对象生命周期

堆空间上的对象声明周期主要分为三个阶段:

  • 内存分配:声明引用变量的时候,在堆上创建内存空间;
  • 内存使用:使用引用变量;
  • 内存回收:引用变量不再使用后,由GC回收堆上的内存空间。

举个例子:

var obj = {id:"object"}; //在堆上分配一段内存空间存放对象
alert(obj.id); 
obj = null; //告诉GC释放堆上的内存

这些概念理解起来仍然太抽象,结合例子来理解:

引用名obj就是mutator root,它存放在栈上,通过obj可以访问到对象中的数据;
声明var obj = {id:"object"}; 的时候,allocator会在堆上分配一段内存空间存放{id:"object"};
mutator就是我们写的代码程序;
在执行obj=null前,我们可以通过obj这个mutator root访问到{id:"object"},{id:"object"}是可达对象;
执行obj=null后,{id:"object"}变成了不可达对象,由collector回收。
垃圾回收操作什么时候执行?

我们写的代码并不是不间断执行的,每执行一段时间,就会周期性地停下来转而去执行垃圾回收操作。上例obj=null;执行结束后,{id:"object"}所在内存没有立即被释放,而是在下次执行垃圾回收操作的时候释放。

2 Javascript垃圾回收

2.1 引言

不同于C++Javascript虚拟机有垃圾回收机制,因此我们经常在网上看到这么一句话:Javascript不需要用户管理内存。然而,这句话是错误的!Javascript使用不当也会导致内存泄露,
内存泄露的概念:内存泄露是指用户申请的、不再使用的内存没有得到及时释放,导致程序运行期间,内存占用越来越大。

2.2 基本概念解析

2.2.1 垃圾回收

在说这个东西之前,先要解释什么是内存泄漏,因为内存泄漏了,所以引擎才会去回收这些没有用的变量,这一过程就叫垃圾回收

2.2.2 内存泄漏

程序的运行需要占用内存,当这些程序没有用到时,还不释放内存,就会引起内存泄漏。举个通俗的例子,就好比占着茅坑不拉屎,坑位(内存量)就这么多,你还不出去(释放内存),就会引起想拉的人不能拉(系统变卡,严重点的会引起进程崩溃)
也就是说不再用到的内存,没有及时释放,就被称为内存泄漏。而内存泄漏,会让系统占用极高的内存,让系统变卡甚至奔溃。所以会有垃圾回收机制来帮助我们回收用不到的内存

当我们遇到遇到内存泄漏时,我们需要做什么呢?
不需要做任何事,因为 JavaScript 中的垃圾回收是自动的

自动内存管理(垃圾回收)阵营:
JavaScript、Java、Go、Python、PHP、Ruby、C#
手动内存管理阵营:
C、C++、Rust

2.2.3 垃圾回收运行机制

JavaScript 的数据类型可分为基本类型和引用类型。基本类型存在栈内存,引用类型存在堆内存
JavaScript 中,引擎需要用栈来维护程序执行时的上下文状态(即执行上下文),如果栈空间大了的话,所有数据存放在栈空间中,会影响到上下文切换的效率,从而影响整个程序的执行效率,所以栈内存大的数据会放在堆空间中,引用它的地址来表示这个变量

2.3 GC模块

var obj = {id:"object"}; //在堆上分配一段内存空间存放对象
alert(obj.id); 
obj = null; //告诉GC释放堆上的内存

GC主要组成模块如下:

  • allocator:职责是分配内存;
  • mutator root:是存放在栈上的、对堆上数据的引用,可以直接被mutator访问到;
  • mutator:可以通过allocator创建对象,通过mutator root读写堆数据;
  • 可达对象:GC从mutator root开始进行图遍历,可以被访问到的对象都称为是可达对象;
  • collector:用于回收不再使用的内存——不可达对象。

2.4 常见GC算法

回收算法 产生背景或算法思想
引用计数法 它是早期IE浏览器引擎JScript采用的垃圾回收算法。
标记清除法 引用计数法会因为循环引用产生内存泄露,为解决这个问题,提出了标记清除法。
标记压缩法 为了解决标记清除法引起的内存碎片问题,提出了标记压缩法。
复制算法 复制算法同样解决了标记清除法的内存碎片问题,它与标记压缩法各有优劣,两者适用于不同的场景。
并行回收 为减少垃圾回收时间,提出并行回收算法,在垃圾回收阶段启用多个线程执行垃圾回收操作。
增量标记 js每执行一段时间,就会停顿下来执行一次垃圾回收操作,为了防止js代码过长时间不被执行,把每次的垃圾回收操作拆分为多个时间片,每执行完一个时间片后就返回执行js代码。
延迟清理 如果当前内存足以支持代码的快速运行,可让程序先运行,等内存不足的时候再进行清理。
分区思想 不同分区采用不同的回收算法。
分代思想 是分区算法的一种变种,不同代使用不同回收算法,当某个代中的数据达到某一条件后,就把数据拷贝到另一代中。

2.4.1 引用计数(reference counting)

简单来说:引擎会有张引用表,保存了内存里面的资源的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放
核心思想:设置引用数,判断当前引用数是否为0,引用计数器;引用关系改变时就会修改引用数字,比如有一个内存空间有一个变量指向它引用计数就会加一,如果这个变量不再指向它了引用计数就会减一,当这个内存空间引用数字为0时立即回收。

引用计数算法的优点:

  • 发现垃圾时立即回收
  • 最大限度减少程序暂停(应用程序在执行的过程中会对内存进行消耗,内存是有限制的,当内存将要爆满的时候引用计数就会立即找到引用数0的内存空间立即释放)

引用计数算法的缺点:

  • 无法回收循环引用的对象,由于循环引用会导致内存泄漏
  • 时间开销大(引用计数要维护着引用数的变化,时刻监控当前对象的引用数值是否需要修改,如果内存中有非常多的对象需要修改,那么时间开销会大一些)

2.4.2 标记清除算法

标记清除Mark-Sweep)主要分为两个阶段:标记清除

  • 标记阶段
    在标记阶段会遍历堆中的所有对象,然后标记活着的对象。Sweep算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收,具体步骤如下:
    • 垃圾回收器会在内部构建一个根列表,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window全局对象可以看成一个根节点。
    • 垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
  • 清除阶段
    该阶段则是将标记阶段中不能访问的变量进行清理
  • 回收相应的空间

看下图来理解标记清除算法:
我们都知道标记清除算法标记的都是可达对象,可达的标准就是全局作用域Global下查找到的对象就是可达对象。下面来仔细看图,图中global是全局作用域就是根,下面的 A B C D E都是可达对象,而右边的obj1 和 obj2 在局部作用域中并且两个互相引用,不是可达对象无法进行标记就会被清除掉,其实标记清除算法也就解决了上述中的引用计数算法 的无法回收循环引用对象的问题。将回收的空间放在空闲链表的地方。
在这里插入图片描述

标记清除算法优点:

  • 相对于引用计数算法,可解决循环引用对象的问题

标记清除算法缺点:

  • 标记清除算法的空间回收,地址不连续会导致空间碎片化
  • 不会立即回收垃圾对象(清除的时候程序是停止工作的)

如下图所示通过标记清除算法标记了可达对象B,而对象A和对象C都是不可达的,就会被回收掉他们的内存空间,但是B的内存空间正好在A和C的中间位置 这样就会导致回收的空间地址不连续的,比如对象D空间大小正好是2或者1就会被分配到A或C,如果D的空间大小是1.5那么找A的空间就会太大,而找C的空间就会太小,这样会导致内存空间会有很多碎片。
在这里插入图片描述

2.4.3 标记整理算法

由于标记清除算法存在一个问题,就是在经历过一次标记清除后,内存空间可能会出现不连续的状态,因为我们所清理的对象的内存地址可能不是连续的,所以就会出现内存碎片的问题,导致后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,而这次垃圾回收其实是没必要的,因为我们确实有很多空闲内存,只不过是不连续的。
为了解决这种内存碎片的问题,Mark-Compact(标记整理)算法被提了出来,该算法主要就是用来解决内存的碎片化问题的,回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的另一端进行移动,移动完成后再清理掉边界外的全部内存

标记整理算法:

  • 标记整理可以看做是标记清除的增强
  • 标记阶段的操作和标记清除一致(遍历所有对象找标记活动对象(活动对象:可达对象))
  • 清除阶段会先执行整理,移动对象位置

标记整理优缺点:

  • 减少碎片化空间
  • 不会立即回收垃圾对象(清除的时候程序是停止工作的)

2.4.4 分代算法

分代算法:

  • 新生代(new space
    主要由两个semispace(半空间)构成,from区域、to区域。内存最大值在64位系统和32位系统上分别为32MB和16MB,在新生代的垃圾回收过程中主要采用了Scavenge算法(复制算法)。
    对象创建时都会被分配在from区域,然后在gc(Garbage Collection)阶段,会将from中还存活的数据复制到to区域,然后交互to区域和from区域。实现一次垃圾回收。如果在两次gc中都还存活的数据就会被移动到老生代区域存储。
    新生代的gc频率很高、速度快、但是空间利用率底,典型的空间换时间的做法。

  • 新生代中的数据晋升
    当一个对象在经过多次(两次)复制之后依旧存活,那么它会被认为是一个生命周期较长的对象,在下一次进行垃圾回收时,该对象会被直接转移到老生代中,这种对象从新生代转移到老生代的过程我们称之为晋升。
    在新生代中的数据晋升需要满足下面两个条件中的任意一个:

    • 对象是否经历过一次Scavenge算法,如果是则放入老生代数据中
    • to区域的剩余的内存占比是否已经低于25%,如果是则放入到老生代中
  • 老生代(old space
    新生代中多次回收仍然存活的对象会被转移到空间较大的老生代。因为老生代空间较大,如果回收方式仍然采用 Scanvage 算法来频繁复制对象,性能开销会非常大,不适用于这种场景。
    现在在老生代中主要适用标记清除Mark-Sweep)来进行gc。在以前使用引用计数来进行gc,但是该方式容易造成内存泄漏,主要表现为循环引用的情况下,数据不能被正常的回收,导致内存飙升,造成内存泄漏。

2.4.5 增量标记(Incremental Marking)

由于JS的单线程机制,垃圾回收的过程会阻碍主线程同步任务的执行,待执行完垃圾回收后才会再次恢复执行主任务的逻辑,这种行为被称为 全停顿(stop-the-world)
在标记阶段同样会阻碍主线程的执行,一般来说,老生代会保存大量存活的对象,如果在标记阶段将整个堆内存遍历一遍,那么势必会造成严重的卡顿。
因此,为了减少垃圾回收带来的停顿时间,V8引擎又引入了Incremental Marking(增量标记)的概念,即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。这个理念其实有点像React框架中的Fiber架构,只有在浏览器的空闲时间才会去遍历Fiber Tree执行对应的任务,否则延迟执行,尽可能少地影响主线程的任务,避免应用卡顿,提升应用性能。

得益于增量标记的好处,V8引擎后续继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能

2.5 V8中的GC算法

2.5.1 V8简介

什么是V8:

  • V8是一款主流的JavaScript执行引擎
  • V8采用即时编译(一般的JS引擎源代码- 字节码才会执行,而V8会直接翻译成机器码)
  • V8内存设有上限的(64位 ≤ 1.5G;32位 ≤ 800M )

2.5.2 V8垃圾回收策略

V8垃圾回收策略:

  • 采用分代回收的思想
  • 内存分为新生代、老生代
  • 针对不同对象采用不同算法

V8的回收算法复杂,它是上述各种基本GC算法的集大成者:

  • 我们从V8内存模型图可以看出,V8的堆空间被分为了多个区,不同的区采用不同的回收算法,这里便用到了回收算法中的分区思想
  • 新生代和老生代内部采用了不同的回收算法,如果新生代中的一些数据经过几轮回收之后仍然存在于内存中,就把这些数据移动到老生代,这里用到了分代思想
  • 新生代中的对象主要通过 Scavenge 算法进行垃圾回收,在 Scavenge 的具体实现中,主要采用 Cheney 算法。Cheney属于GC算法中的复制算法
  • 老生代中的对象通过标记清除法标记压缩法回收;
  • 根据前面的讲解,我们已经知道,js代码每执行一段时间,就会停顿下来,转而去执行垃圾回收操作,为了减少停顿时间,加快代码响应速度,V8采用了并行回收,增量标记,以及延迟清理三种算法。

2.5.3 V8回收新生代

首先我们先看一下V8的内存分配,如下图所示左侧红色区域专门存储新生代存储区,右侧为老生代存储区

  • V8内存空间一分为二
  • 小空间用于存储新生代对象(64位→32M | 32位→16M)
  • 新生代指的是存活时间较短的对象 (什么是存活时间较短的对象:当前的代码内有一个变量a在局部作用域,变量b在全局作用域,a的存活时间是比较短的)

新生代对象回收实现:

  • 回收过程采用复制算法
  • 新生代内存区分为二个等大小空间 FromTo
  • 使用空间为From,空闲空间为To
  • 活动对象存储于 From 空间
  • 标记整理后将活动对象拷贝至To空间,From 空间的活动对象就会有一个备份
  • FromTo交换空间完成释放

拷贝过程中可能出现晋升,晋升就是将新生代对象移动至老生代,如果一轮GC还存活的新生代需要晋升,如果To空间的使用率超过25%将新生代对象移动至老生代
在这里插入图片描述

2.5.4 V8回收老生代

V8如何回收老生代呢:

  • 老生代64位→1.4G , 32位→ 700M
  • 老生代对象就是指存活时间较长的对象(如全局作用域下所存放的变量、闭包的情况下所存储的变量数据)
  • 主要采用:标记清除标记整理增量标记算法
    • 首先使用标记清除完成垃圾空间的回收
    • 采用标记整理进行空间优化(当新生代区域内容移动至老生代区域,而且老生代的存储空间不足以存储新生代所移动过来的对象,就会执行标记整理优化空间)
    • 采用增量标记进行效率优化

细节对比:

  • 新生代区域垃圾回收使用空间换时间
  • 老生代区域垃圾回收不适合复制算法

关于增量标记算法如何优化垃圾回收?
如下图示
在这里插入图片描述

分会两个部分一个是程序的执行一个是垃圾回收,当执行垃圾回收操作会停止程序的执行,将一整段的垃圾回收操作组合的完成垃圾回收,垃圾回收与程序执行交替执行这样所带来的时间消耗会合理一些,程序执行一会标记一轮,最后标记操作完成操作后就进行垃圾回收操作,当垃圾回收操作完成之后程序继续执行操作。以前的垃圾回收会进行一整段操作,也会使程序停顿很长的一段时间。

回顾V8垃圾回收:

  • V8是一款主流的JavaScript执行引擎
  • V8内存设置上限 主要针对浏览器
  • V8采用基于分代回收思想实现垃圾回收
  • V8内存分为新生代和老生代
  • V8垃圾回收常见的GC算法(新生代:复制算法+标记整理;老生代:标记清除 + 标记整理 + 增量标记)

2.6 新生代与老生代的垃圾回收

在这里插入图片描述

在介绍两种垃圾回收机制前,要先知道两个知识点:代际假说分代收集

代际假说有以下两个特点:

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

因为有代际假说的认知,所以我们在垃圾回收时,会根据对象不同的生存周期采用不同的算法,其中 V8堆内存分为新生代和老生代两个区域(其他几个区域用处不大)

新生代中存放生存时间短的对象,老生代存放生存时间久的对象
为此,新生代区通常只支持1~8M 的容量,而老生代区会支持更大的容量,而针对这两块区域,V8 分别使用两个不同的垃圾回收器

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

2.6.1 新生代内存回收

新生代采用的是 Scavenge 算法,所谓 Scavenge 算法,是把新生代空间对半分为两个区域,一半是对象区域(from),一半是空闲区域(to)。如下图所示:
在这里插入图片描述

新的对象会首先被分配到对象(from)空间,当对象区域快写满时,就需要执行一次垃圾清理操作。当进行垃圾收回时,先将 from 空间中存活的对象复制到空闲(to)空间进行保存,对未存活的空间进行回收。复制完成后,对象空间空闲空间进行角色调换,空闲空间变成新的对象空间,原来的对象空间则变成空闲空间。这样就完成了垃圾对象的回收操作,同时这种角色调换的操作能让新生代中的这两块区域无限重复使用下去
在这里插入图片描述
而当一个对象在两次变换中还存在时,就会从新生代区晋升到老生代区。这一过程被称为对象晋升策略
在这里插入图片描述

2.6.2 老生代内存回收

主垃圾回收器负责老生代区的垃圾回收。其中的对象包括新生代区晋升的对象和一些大的对象。因此老生代区中的对象有两个特点,对象占用空间大,对象存活时间长

它不会像新生代区那样使用 Scavenge 算法,因为复制大对象所花费的时间长,执行效率并不高。所以它采用标记 - 清除Mark - Sweep)进行垃圾回收
简单来说,先标记,后清除,但是内存空间里的对象还是不连续,所以引入整理。这就是老生代区的垃圾回收过程 标记 - 清除 - 整理。先标记哪些是要回收的变量,再进行回收(清除),然后将内存空间整理(到一边),这样空间就大了
标记整理过程

因为老生代区的对象相对大,虽然采用标记-清除算法会比 Scavenge 更快,但架不住卡顿问题。为什么会卡顿?因为 JavaScript 是单线程。为此,为了避免垃圾回收时间过长影响其他程序的执行,V8 将标记过程分为一个个子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,这一算法被称为增量标记算法
在这里插入图片描述

在这里插入图片描述

而这一行为,与 React Fiber 的设计思路类似,将大人物分割成小任务,因为小,所以执行快,让人察觉不到卡顿

2.6.3 新生代 VS 老生代

新生代垃圾回收是临时分配的内存,存活时间短;老生代垃圾回收是常驻内存,存活时间长
新生代垃圾回收由副垃圾回收器负责;老生代垃圾回收由主垃圾回收器负责
新生代采用 Scavenge 算法;老生代采用标记-清除算法

  • Scavenge 算法:将空间分为两半,一半是 from 空间,一半是 to 空间。新加入的对象会放在 from 空间,当空间快满时,执行垃圾清理;再角色调换,再当调换完后的 from 空间快满时,再执行垃圾清理,如此反复
  • 标记-清理-整理:此为两个算法,标记-清理算法和 标记-整理算法
    • 标记-清理:标记用不到的变量,清理掉
    • 标记-整理:清理完内存后,会产生不连续的内存空间,为节省空间,整理算法会将内存排序到一处空间,空间就变大了