Node系列 — v8引擎堆内存(二) 垃圾回收机制

1,263 阅读6分钟

Node系列 — v8引擎堆内存详解(一)

看完本文你将学到什么

  • v8 堆内存分代机制
  • 分代晋升机制
  • 垃圾回收机制,涉及算法说明及其比较

了解堆内存及垃圾回收的意义

V8 对内存的限制对于浏览器而言,每个浏览器选项卡会实例化一个 V8 实例,用户选项卡使用周期也不长,内存一般绰绰有余,如果存在内存泄露问题,有一定的缓冲时间。但对于 Node 服务器端而言,生命周期很长,服务的稳定性及其重要,如果存在内存问题,会影响服务的正常运行。V8 的垃圾回收机制是会让 js 线程“全停顿”的,所以垃圾回收也是影响性能的重要因素之一。

v8的内存分代

v8 中主要将内存分为两块空间:

  • 新生代空间(存放生命周期短的对象)
  • 老生代空间(存放生命周期长的对象)

如图: image.png

前面我们提到的 --max-old-space-size--max-new-space-size 两个参数就是分别设置这两块空间的最大值。

《深入浅出Nodejs》中指出: 老生代 内存空间最大值在64位系统和32位系统上分别为 1400MB 和 700MB, 新生代 内存空间最大值在64位系统和32位系统上分别为 32MB 和 16MB。

由此可以解释 v8内存: 老生代 1.4G = (1400MB + 32MB) / 1024MB。 新生代 0.7G = (700MB + 16MB) /1024MB。 至于目前这块内存默认设置是多少,大家有兴趣可以去调查一下😊

垃圾回收机制

Javascript 具有自动垃圾回收机制,不像 CC++ 之类的语言,需要开发者手动跟踪内存情况并进行垃圾回收♻️,这也是很多 javascript 开发者没有关注这一块根本原因。 V8 中的垃圾回收策略重要是基于分代机制。因为在实际生产中,堆内存中对象的生命周期长短不一,如果定期的标记清除,会对生命周期比较长的对象重复处理,垃圾回收的过程 js 线程会暂停执行,导致程序运行性能大大降低。V8 设计者结合统计学对堆内的对象进行生命周期分类,在结合相应的算法处理,对新生代和老生代分别有一套处理机制。

新生代垃圾回收

Scavenge 算法

新生代中主要通过 Scavenge 算法进行垃圾回收,具体实现采用了 Cheney 算法。 Cheney 算法将一块内存一分为二,一块处于使用状态(称为 From 空间),另一块处于闲置状态(称为 To 空间),当我们 js 线程执行要开始分配对象时,会首先在 From 空间分配。开始一轮垃圾回收时,先检查 From 中存活的对象然后复制到 To 中,接着释放 From 中的占用的空间,复制完成后,FromTo 所代表的空间发生互换。

image.png Scavenge 算法

  • 缺点:内存空间只能使用一半
  • 优点:效率特别高

典型的拿空间换时间,这也就限制了该算法不能在全局的垃圾回收中使用。因为新生代生命周期短,该算法就很适合。

那这里有个问题,随着一轮又一轮回收,生命周期很长的对象就一直待在新生代空间🧐,新生代空间不就越来越小??

晋升机制

晋升的意义:就是将新生代中达到一定标准的对象移动到老生代空间中,给新生代腾出空间。 晋升的条件主要有两个:

  1. 对象是否经历过 Scavenge 回收
  2. To 空间的占用大小是否超过 25%

晋升的过程:在执行一次新生代垃圾回收,将存活对象从 From 空间复制到 To 空间的时候,会检查该对象是否经历过一次 Scavenge 回收,如果经历过,则将该对象从 From 空间复制到老生代空间,如果没有,则复制到 To 空间。 另一个判断条件,当从 From 空间复制对象到 To 空间时,如果当前 To 空间的使用比例达到 25%,则直接将该对象复制到老生代空间。

老生代垃圾回收

老生代中垃圾回收主要涉及两个算法 Mark-SweepMark-Compact。 为什么不采用 Scavenge 算法呢🧐 ?主要有两点:

  1. 老生代中大部分是活着的对象,复制大量活着的对象影响回收性能。
  2. 老生代中需要划分一半的空间实现 Scavenge 算法机制,浪费空间。
Mark-sweep 算法

Mark-sweep 算法(标记-清除)会先标记内存中活着的对象,然后清除没有被标记的对象。

标记后如图所示: image.png

清除后如图所示: image.png

Mark-sweep 算法存在一个问题,当清理完死对象后,会存在内存空隙,也就是说内存的状态是不连续的。这时如果分配一个比较大对象到内存,而且各空隙不足以分配,会导致提前触发垃圾回收,这样的垃圾回收是没有必要的。所以这种情况下,就需要 Mark-Compact 算法。

Mark-Compact 算法

Mark-Compact 算法(标记-整理)会先标记内存中死亡的对象,然后往一端移动活着的对象,移动完成后,清理掉边界外的内存。

标记移动如图所示:

image.png

清理完如图所示: 移动完成后,清理掉边界外的内存

image.png

在 v8 中,对于这两种算法是灵活使用的,当可用最大片段空间不足以分配晋升对象时才使用 Mark-Compact 算法。

算法比较

回收算法Mark-SweepMark-CompactScavenge
速度中等最慢最快
空间开销少(有碎片空间)少(无碎片)双倍空间(无碎片)
是否移动对象

垃圾回收执行策略

由于垃圾回收过程中 js 线程会暂停执行,必须等回收完再恢复执行,这种行为被称为“全停顿”。V8 回收过程中,新生代存活对象少,而且内存空间不大,回收基本不影响,而在老生代中存活对象多,而且内存空间大,标记、移动、清理过程将损耗较多时间。V8 在面临这种问题做出进步一改善,在标记阶段的动作从全局标记改为增量标记(incremental marking),将标记动作分为很多小步,每完成一步清理,就恢复 js 应用执行,如此交替,直到标记完成。

总结

image.png

内存泄露是前端开发工程师经常提到的,了解 V8 堆内存及垃圾回收,能让我们理解其内存大小的合理限制,以及内存状态对于我们日常开发的重要性,也能让我们在高性能的代码成长上迈出重要的一步。