Chrome 与 Node.js 都在使用的 JS 引擎 —— V8 之垃圾回收机制(下篇)

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动。
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

国庆放假,身在厦门的我积极响应防疫政策,遂在家好好学习。新习得关于在 Chrome 浏览器和 Node.js 中使用的 js 引擎 V8 的垃圾回收机制,故而在此分享。篇幅较长,分为上下两篇,本篇接上篇继续介绍 V8 的老生代算法 mark-sweep & mark-compact,新生代如何晋升老生代和如何查看并扩充 V8 内存的内容。有不足之处或是任何意见建议,欢迎各位大佬不吝斧正~

老生代算法(mark-sweep & mark-compact)

mark-sweep

V8 引擎早期使用的算法,标记清除算法。首先,垃圾回收机制会有个根节点 GC Roots(注意这里说的根节点不是浏览器环境的 window 或是 node 环境的 global),所有的变量都会被根节点所引用。老生代的垃圾回收,一开始会进行广度扫描,然后将与 GC Roots 有关联的变量进行标记(下图红色矩形块),剩下的没有被标记的变量(下图灰色矩形块)就被认为是垃圾对象,在清除阶段被 GC 清除,新的变量就可以存放了(下图白色矩形块)。图示如下: image.png

mark-compact

V8 引擎现在所采用的算法,标记整理算法。首先也是会做广度扫描然后对有用的变量进行标记,然后不同于 mark-sweep 的直接清除垃圾对象,而是先对变量进行整理,将存活的变量移到一起,形成连续的内存空间(这样就更易于存储诸如数组这种要求连续存储空间的对象),最后将其余空间里的对象清除。
image.png
注意,是先整理后清除,而不是先清除再整理。因为先整理的时候,在移动存活变量时,就把对应位置的垃圾对象覆盖了,这样清除的时候工作量就减少了,效率更高。

全停顿标记与增量标记 + 三色标记法

早期 V8 引擎在标记时采用的是全停顿标记,就是一次性全部标记好。而现在采用的增量标记 + 三色标记法的形式,线程切换频率更高但每次标记的层数更少。比如第一次从运行代码切换到 GC 时,只标记第一层,将该层标记为灰色,然后与之关联的下一层标记为黑色,然后回到主线程代码继续运行。等到下一次切换到 GC 时,直接将上一次置灰的节点作为根节点开始扫描,并且标记为白色,将上一次置黑的节点置灰,将与之关联的下一层置黑,然后再次回到主线程运行代码。如此交替进行,最后置白的都是需要保留的变量。 这样每次执行 GC 的时间就会很短,令人不易察觉。
image.png

新生代晋升到老生代

晋升的条件有两个:

  1. 新生代 Semi Space From 空间内的对象是否经历过一次 scavenge 回收,也就是有没有被复制过;
  2. Semi Space To 空间使用已经超过了 25%

在老生代中对象相比于新生代比较稳定,存活时间较长。

查看内存使用情况

浏览器中

可以直接在 Chrome 浏览器的控制台中输入 window.performance.memory 查看:

image.png
注意单位是 b,转换成 MB 可以看到是 2072MB,也就是 2GB左右

node 中

可以通过 process.memoryUsage() 得到 Node.js 进程的内存使用量(单位是 b)的对象。

image.png

以下解释引用自 Node.js v16.10.0 文档:

  • rss,常驻集大小,是进程在主内存设备(即总分配内存的子集)中占用的空间量,包括所有 C++ 和 JavaScript 对象和代码。
  • heapTotal 和 heapUsed 指的是 V8 的内存使用量。
  • external 指的是绑定到 V8 管理的 JavaScript 对象的 C++ 对象的内存使用量。

扩充内存

当我们的项目内存不够时,可以对内存进行扩充:比如想扩充到 4GB,可以在启动时执行: node --max-old-space-size=4096 要执行的文件.js 即可,单位是 MB。 这里顺便提一句,全局对象里的变量是不会被垃圾回收的,会一直存在到当前线程结束,所以尽量不要把变量定义在全局对象里。

感谢.gif 点赞.png

猜你喜欢

转载自juejin.im/post/7014774486746529799