理解 Node.js 的 GC 机制

《深入浅出Node.js》第五章《内存控制》阅读笔记

随着 Node 的发展,JavaScript 的应用场景早已不再局限在浏览器中。本文不讨论网页应用、命令行工具等短时间执行,且只影响终端用户的场景。由于运行时间短,随着进程的退出,内存会释放,几乎没有内存管理的必要。但随着 Node 在服务端的广泛应用,JavaScript 的内存管理需要引起我们的重视。

V8 的内存限制

在一般的后端开发语言中,在基本的内存使用上没有什么限制,然而在 Node 中通过 JavaScript 使用内存时就会发现只能使用部分内存(64位系统下约为1.4GB,32位系统下约为0.7GB)。在这样的限制下,将会导致 Node 无法直接操作大内存对象。

造成这个问题的主要原因在于 Node 的 JavaScript 执行引擎 V8。

在 V8 中,所有的 JavaScript 对象都是通过堆来进行分配的。Node 提供了 V8 中内存的使用量查看方法 process.memoryUsage()

  • heapTotal 已申请到的堆内存
  • heapUsed 当前使用的堆内存

为什么 V8 要限制堆的大小:

  1. V8 为浏览器而设计,不太可能遇到用大量内存的场景
  2. V8 的垃圾回收机制的限制。(按官方的说法,以1.5GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50ms以上,做一次非增量式的垃圾回收需要1s以上)

V8提供了选项让我们可以控制使用内存的大小

  • node --max-old-space-size=1700 test.js 设置老生代内存空间最大值,单位为MB
  • node --max-new-space-size=1024 test.js 设置新生代内存空间最大值,单位为KB

比较遗憾的是,这两个最大值需要在启动时执行。这意味着 V8 使用的内存没办法根据使用的情况自动扩充,当内存分配过程中超过极限值时,就会引起进程出错。

V8 的垃圾回收机制

V8 的垃圾回收策略主要基于分代式垃圾回收机制。在 V8 中,主要将内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

V8的分代示意图

V8 堆的整体大小就是新生代的内存空间加上老生代的内存空间

Scavenge 算法

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

Cheney 算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间成为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置中。处于使用中的 semispace 空间成为 From 空间,处于闲置状态的空间成为 To 空间。当我们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,而非存活对象占用的空间将被释放。完成复制后, From 空间和 To 空间的角色发生对换。

Scavenge 的缺点是只能使用堆内存的一半,但 Scavenge 由于只复制存活的对象,并且对于生命周期短的场景存活对象只占少部分,所以它在时间效率上表现优异。Scavenge 是典型的牺牲空间换取时间的算法,无法大规模地应用到所有的垃圾回收中,但非常适合应用在新生代中。

V8中的堆内存示意图

晋升

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

From 空间中的存活对象在复制到 To 空间之前需要进行检查,在一定条件下,需要将存活周期长的对象移动到老生代中,也就是完成对象的晋升。

晋升条件主要有两个:

  1. 对象是否经历过一次 Scavenge 回收
  2. To 空间已经使用超过 25%

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

Mark-Sweep & Mark-Compact

V8 在老生代中主要采用了 Mark-Sweep 和 Mark-Compact 相结合的方式进行垃圾回收。

Mark-Sweep 是标记清除的意思,它分为两个阶段,标记和清除。Mark-Sweep 在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除未被标记的对象。

Mark-Sweep 最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题,因为很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。

为了解决 Mark-Sweep 的内存碎片问题,Mark-Compact 被提出来。Mark-Compact是标记整理的意思,是在 Mark-Sweep 的基础上演进而来的。它们的差别在于对象在标记为死亡后,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

下表为3种主要垃圾回收算法的简单比较

从表中可以看出,在 Mark-Sweep 和 Mark-Compact 之间,由于 Mark-Compact 需要移动对象,所以它的执行速度不可能很快,所以在取舍上,V8 主要使用 Mark-Sweep,在空间不足以从新生代中晋升过来的对象进行分配时才使用 Mark-Compact 。

Incremental Marking

为了避免出现 JavaScript 应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种算法都需要将应用逻辑暂停下来,这种行为称为“全停顿” (stop-the-world)。

由于新生代配置的空间较小,存活对象较少,全停顿对新生代影响不大。但老生代通常配置的空间较大,且存活对象较多,全堆垃圾回收(full 垃圾回收)的标记、清除、整理等动作造成的停顿就会比较可怕。

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

V8 在经过增量标记的改进后,垃圾回收的最大停顿时间可以减少到原本的 1/6 左右。

查看GC日志

查看垃圾回收日志的方式主要是在启动时添加 --trace_gc 参数。

小结

  1. Node 的 JavaScript 执行引擎为 V8,内存使用和控制也受限于 V8。
  2. V8 把内存分为新生代和老生代,分别存放存活时间较短和存活时间较长或常驻内存的对象。
  3. 在新生代中使用 Scavenge 算法进行垃圾回收,优点是速度快无内存碎片,缺点是占用双倍内存空间。
  4. 在老生代中将 Mark-Sweep 和 Mark-Compact 两种算法结合使用,主要使用 Mark-Sweep,优点的是无需移动对象,缺点是产生内存碎片。Mark-Compact 是对 Mark-Sweep 的补充,在空间不足以对新晋升的对象进行分配时整理内存,清除内存碎片,由于要移动对象,速度较慢。
  5. V8 使用 Incremental Marking 来减少全停顿带来的影响。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352