V8引擎的垃圾回收机制

在这里插入图片描述

在服务端,资源向来寸土寸金,要为海量用户服务,就得使一切资源都要高效循环利用。

V8 的垃圾回收机制与内存限制

JavaScript 的垃圾回收由垃圾回收机制进行自动内存管理,使得开发者不需要在编写代码的过程中时刻关注内存的分配和释放问题。

对于性能敏感的服务端程序,内存管理的好坏,垃圾回收状态是否优良,都会对服务构成影响。在 Node 中,这一切都与 Node 的 JavaScript 执行引擎 V8 息息相关。

Node 与 V8

V8 出现后,JavaScript 一改之前作为脚本语言性能底下的形象。在性能跑分中,V8 持续领先。V8 的性能优势使得用 JavaScript 写高性能后台服务程序成为可能。Ryan Dahl 选择了 JavaScript,选择了 V8,在事件驱动,非阻塞 I/O 模型的设计下实现了 Node。

V8 的内存限制

一般的后端开发语言在内存上的使用基本没有什么限制。但在 Node 中通过 JavaScript 使用内存时就会发现只能使用部分内存。造成这个问题的主要原因在于 Node 基于 V8 构建,所以在 Node 中使用的 JavaScript 对象基本上都是通过 V8 自己的方式来进行内存分配和管理。

V8 对象分配

在 V8 中,所有的 JavaScript 对象都是通过来进行分配的。在 Node 环境下可以使用 process.memoryUsage() 来查看 V8 中内存使用量。

C:\Users\DELL>node
Welcome to Node.js v12.21.0.
Type ".help" for more information.
>
> process.memoryUsage()
{
  rss: 24256512,
  heapTotal: 5222400,
  heapUsed: 3042832,
  external: 1581429,
  arrayBuffers: 148663
}

heapTotal:已申请到的堆内存

heapUsed:已使用的量

rss:进程的常驻部分

在这里插入图片描述

在代码中声明变量并赋值的时候,所使用对象的内存就分配在堆中。

为何 V8 要限制堆的大小?

按官方的说法,以 1.5 GB 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要 50 毫秒以上,做一次非增量式的垃圾回收甚至要 1 秒以上。这是垃圾回收中引起 JavaScript 线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。

(想想平时页面稍微一点点卡,就暴躁了。。。👺👺👺)

V8 的垃圾回收算法👇

V8 的垃圾回收算法主要基于分布式垃圾回收机制。一般来说,没有一种垃圾回收算法可以胜任任何场景。毕竟在实际的应用中,对象的生存周期长短不一,不同的算法只能针对特定的情况才会具备最好的效果。

现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存施以更高效的算法。

内存分代
  • 新生代:其中的对象存活时间较短
  • 老生代:其中的存活时间较长或常驻内存的对象

在这里插入图片描述

Scavenge 算法 ---- 新生代

新生代的对象主要通过 Scavenge 算法进行垃圾回收。Scavenge 算法的具体实现中采用了 Cheney 算法。

Cheney 算法是一种采用复制的方式实现的垃圾回收算法。将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的 semispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。

开始分配对象时,先在 From 中间中进行分配。进行垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,而非存活对象占用的空间将会被释放。

完成复制后,From 空间和 To 空间的角色发生对换(翻转)。

该算法的缺点很明显,只能使用堆内存中的一半,这是由划分空间和复制机制所决定的。但 Scavenge 算法由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以在时间效率上有优异的表现。

可以发现 Scavenge 算法非常适合应用在新生代中,因为新生代中对象的生命周期较短。

在这里插入图片描述

当一个对象经过多次复制依然存活时,它将会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。

对象从新生代移动到老生代中的过程称为晋升。

(有没有很形象😂😂😂)

在单纯的 Scavenge 过程中, From 空间中的存活对象会被复制到 To 空间中去,然后两个空间的角色进行兑换。但在分代式垃圾回收的前提下,From 空间的存活对象在复制到 To 空间之前需要进行检查。如果符合对象晋升的条件,则需要将存活周期长的对象移动到老生代中,完成对象晋升。

晋升条件:一是对象是否经过 Scavenge 回收,二是 To 空间的内存占用比超过 25%

在这里插入图片描述

设置 25% 是因为当这次 Scavenge 回收完成之后,这个 To 空间将变成 From 空间,接下来的内存分配将在这个空间中进行。如果占比过高,将会影响后续的内存分配。

Mark-Sweep & Mark-Compact ---- 老生代

不继续使用 Scavenge 回收的原因

  • 老生代中的对象占比重大,存活对象多,复制存活对象的效率将会很低
  • 浪费一半空间的问题

Mark-Sweep —> 标记清除

分为标记和清除。

**标记:**该阶段会遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。Scavenge 只复制活着的对象,因为活对象在新生代中只占较小的部分。 Mark-Sweep 只清理死亡对象,死对象在老生代中只占较小的部分。

问题:进行一次标记清除之后,内存空间会出现不连续的状态。(一块一块的)对后续的内存分配会造成问题,比如无法对大对象进行内存分配。

Mark-Compact —> 标记整理

Mark-Compact 对象在标记死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

在 V8 的回收策略中两者是结合使用的。

Incremental Marking

以上介绍的3种垃圾回收机制,都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行阴影逻辑,这种行为被称为 “全停顿”(stop-the-world)。

一次小垃圾回收只收集新生代,新生代中存活对象通常较少,即便全停顿影响也不大。但老生代中通常存活对象较多,全堆垃圾回收(full 垃圾回收)的标记,清理,整理等动作造成的停顿就会很可怕。

为了降低全堆垃圾回收带来的停顿事件。V8 在标记阶段时会将原本需要一口气停顿完成的动作改为增量标记(Incremental Marking),即拆分为许多小“步进”,每做完一“步进”就让 JavaScript 应用逻辑执行一会,垃圾回收与应用逻辑交替执行直到标记阶段完成

V8 后续还引入了延迟清理(lazy sweeping)与增量式整理(Incremental compaction),让清理和整理动作也变成增量式的。

查看内存使用情况

使用 process.memoryUsage()可以查看内存使用情况

查看系统的内存占用

os 模块中的 totalmem() (总内存)和 freemem() (闲置内存)可以查看操作系统的内存使用情况

C:\Users\DELL>node
Welcome to Node.js v12.21.0.
Type ".help" for more information.
> os.totalmem()
8482263040
> os.freemem()
2865393664
堆外内存

不是通过 V8 分配的内存称为堆外内存。Buffer 对象不同于其他对象,不需要经过 V8 的内存分配机制,所以也不会有堆内存的大小限制。

Node 的内存主要通过 V8 进行分配的部分和 Node 自行分配的部分构成。受 V8 的垃圾回收限制的主要是 V8 的堆内存。

内存泄漏

内存泄漏是指我们已经无法再通过 js 代码来引用到某个对象,但垃圾回收器却认为这个对象还在被引用,因此在回收的时候不会释放它。导致了分配的这块内存永远也无法被释放出来。如果这样的情况越来越多,会导致内存不够用而系统崩溃。

造成内存泄露的情况有很多,但本质只有一个,应该回收的对象出现意外而没有被回收,变成了常驻在老生代的对象。

通过造成内存泄露的原因:

  • 缓存
  • 队列消费不及时
  • 作用域未释放
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值