深入理解V8的垃圾回收原理

垃圾回收机制

V8的垃圾回收策略基于分代回收机制,该机制又基于 世代假说。该假说有两个特点:

  • 大部分新生对象倾向于早死;
  • 不死的对象,会活得更久。

基于这个理论,现代垃圾回收算法根据对象的存活时间将内存进行了分代,并对不同分代的内存采用不同的高效算法进行垃圾回收。

V8的内存分代

在V8中,将内存分为了新生代(new space)和老生代(old space)。它们特点如下:

  • 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
  • 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。

V8堆的空间等于新生代空间加上老生代空间。我们可以通过 --max-old-space-size命令设置老生代空间的最大值,--max-new-space-size 命令设置新生代空间的最大值。老生代与新生代的空间大小在程序初始化时设置,一旦生效则不能动态改变。

node --max-old-space-size=1700 test.js // 单位为 MB
node --max-new-space-size=1024 test.js // 单位为KB

默认设置下,64位系统的老生代大小为1400M,32位系统为700M。
对于新生代,它由两个 reserved_semispace_size 组成。每个reserved_semispace_size 的大小在不同位数的机器上大小不同。默认设置下,在64位与32位的系统下分别为16MB和8MB。我们将新生代、老生代、reserved_semispace_size 空间大小总结如下表。

类型 \ 系统位数 64位 32位
老生代 1400MB 700MB
reserved_semispace_size 16MB 8MB
新生代 32MB 16MB

回收算法

Stop The World (全停顿)

在介绍垃圾回收算法之前,我们先了解一下「全停顿」。垃圾回收算法在执行前,需要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑,这种行为称为 「全停顿」(Stop The World)。例如,如果一次GC需要50ms,应用逻辑就会暂停50ms。

全停顿的目的,是为了解决应用逻辑与垃圾回收器看到的情况不一致的问题。举个例子,在自助餐厅吃饭,高高兴兴地取完食物回来时,结果发现自己餐具被服务员收走了。这里,服务员好比垃圾回收器,餐具就像是分配的对象,我们就是应用逻辑。在我们看来,只是将餐具临时放在桌上,但是服务员看来觉得你已经不需要使用了,因此就收走了。你与服务员对于同一个事物看到的情况是不一致,导致服务员做了与我们不期望的事情。因此,为避免应用逻辑与垃圾回收器看到的情况不一致,垃圾回收算法在执行时,需要停止应用逻辑。

Scavenge 算法

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

V8堆内存空间示意图

Cheney算法采用复制的方式进行垃圾回收。它将堆内存一分为二,每一部分空间称为 semispace。这两个空间,只有一个空间处于使用中,另一个则处于闲置。使用中的 semispace 称为 「From 空间」,闲置的 semispace 称为 「To 空间」。

过程如下:

  1. 从 From 空间分配对象,若 semispace 被分配满,则执行 Scavenge 算法进行垃圾回收。
  2. 检查 From 空间的存活对象,若对象存活,则检查对象是否符合晋升条件,若符合条件则晋升到老生代,否则将对象从 From 空间复制到 To 空间。
  3. 若对象不存活,则释放不存活对象的空间。
  4. 完成复制后,将 From 空间与 To 空间进行角色翻转(flip)。

对象晋升

对象晋升的条件有两个:

1)对象是否经历过Scavenge回收。对象从 From 空间复制 To 空间时,会检查对象的内存地址来判断对象是否已经经过一次Scavenge回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。

2)To 空间的内存使用占比是否超过限制。当对象从From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置为25%的比例的原因是,当完成 Scavenge 回收后,To 空间将翻转成From 空间,继续进行对象内存的分配。若占比过大,将影响后续内存分配。

对象晋升到老生代后,将接受新的垃圾回收算法处理。下图为Scavenge算法中,对象晋升流程图。

晋升流程图

小结

Scavenge 算法的缺点是,它的算法机制决定了只能利用一半的内存空间。但是新生代中的对象生存周期短、存活对象少,进行对象复制的成本不是很高,因而非常适合这种场景。

Mark-Sweep & Mark-Compact

老生代中的对象有两个特点,第一是存活对象多,第二个存活时间长。若在老生代中使用 Scavenge 算法进行垃圾回收,将会导致复制存活对象的效率不高,且还会浪费一半的空间。因而,V8在老生代采用Mark-Sweep 和 Mark-Compact 算法进行垃圾回收。

Mark-Sweep

Mark-Sweep,是标记清除的意思。它主要分为标记和清除两个阶段。

  • 标记阶段,它将遍历堆中所有对象,并对存活的对象进行标记;
  • 清除阶段,对未标记对象的空间进行回收。

与 Scavenge 算法不同,Mark-Sweep 不会对内存一分为二,因此不会浪费空间。但是,经历过一次 Mark-Sweep 之后,内存的空间将会变得不连续,这样会对后续内存分配造成问题。比如,当需要分配一个比较大的对象时,没有任何一个碎片内支持分配,这将提前触发一次垃圾回收,尽管这次垃圾回收是没有必要的。

Mark-Sweep 标记与清理的示意图

Mark-Compact

为了解决内存碎片的问题,提高对内存的利用,引入了 Mark-Compact (标记整理)算法。Mark-Compact 是在 Mark-Sweep 算法上进行了改进,标记阶段与Mark-Sweep相同,但是对未标记的对象处理方式不同。与Mark-Sweep是对未标记的对象立即进行回收,Mark-Compact则是将存活的对象移动到一边,然后再清理端边界外的内存。

Mark-Compact 算法

小结

由于Mark-Compact需要移动对象,所以执行速度上,比Mark-Sweep要慢。所以,V8主要使用Mark-Sweep算法,然后在当空间内存分配不足时,采用Mark-Compact算法。

Incremental Marking(增量标记)

在新生代中,由于存活对象少,垃圾回收效率高,全停顿时间短,造成的影响小。但是老生代中,存活对象多,垃圾回收时间长,全停顿造成的影响大。为了减少全停顿的时间,V8对标记进行了优化,将一次停顿进行的标记过程,分成了很多小步。每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成标记。如下图所示:

增量标记示意图

长时间的GC,会导致应用暂停和无响应,将会导致糟糕的用户体验。从2011年起,v8就将「全暂停」标记换成了增量标记。改进后的标记方式,最大停顿时间减少到原来的1/6。

lazy sweeping(延迟清理)

  • 发生在增量标记之后
  • 堆确切地知道有多少空间能被释放
  • 延迟清理是被允许的,因此页面的清理可以根据需要进行清理
  • 当延迟清理完成后,增量标记将重新开始。

总结

垃圾回收的原理较为复杂,在理解上需要花费一些功夫。了解GC原理,有助于我们对NodeJS项目进行性能瓶颈定位与调优。文章所描述的算法为V8中使用的基础算法,现代V8引擎对垃圾回收进行了很多改进,比如,在Chrome 64和Node.js v10中V8启用了「并行标记」技术,将标记时间缩短了60%~70%。还有「Parallel Scavenger」技术,它将新生代的垃圾回收时间缩短了20%~50%。

垃圾回收是影响服务性能的因素之一,为了提高服务性能,应尽量减少垃圾回收的次数。

附录:

(习题) node应用默认最多使用多少内存

V8堆内存最大值在64位系统上为1464MB,在32位系统上为732MB。计算公式如下:

V8堆内存最大保留空间等于:
4 * reserved_semispace_ size_ + max_old_generation_size_

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 前言 我们知道,JavaScript之所以能在浏览器环境和NodeJS环境运行,都是因为有V8引擎在幕后保驾护航。...
    liuxuan阅读 470评论 1 1
  • 今天看了一下关于垃圾回收的知识,来总结一下~我们知道,JavaScript之所以能在浏览器环境和NodeJS环境运...
    Yixi_Li阅读 589评论 0 1
  • V8的垃圾回收机制与内存限制 V8的内存限制 在一般的后端开发语言中,在基本的内存使用上没有什么限制,然而在Nod...
    Upcccz阅读 1,319评论 1 3
  • 本文引用自这里这篇文章的所有内容均来自 朴灵的《深入浅出Node.js》及A tour of V8:Garbage...
    楠小忎阅读 1,342评论 0 2
  • 上一篇文章中,我整理介绍了V8的新生代堆内存的垃圾回收策略,这里再简单概述下:V8将堆内存主要划分为新生代和老生代...
    LK2917阅读 1,172评论 0 3