[译]Orinoco 垃圾收集器

原文链接 https://v8.dev/blog/trash-talk

任何垃圾收集器都有一些必须定期执行的基本任务:
1、识别不再使用/正在使用的对象
2、回收/重用不再使用的对象所占用的内存
3、压缩管理内存

这些任务可以按顺序执行,也可以任意交错。 一种直接的方法是在主程序中暂停JavaScript的执行来顺序执行这些任务,但可能会导致主线程上的卡顿和延迟问题

代际布局

V8中的堆被分成称为代的不同区域。 有新生代(进一步分为“托儿所”和“中间”子代)和老年代。 对象首先分配到托儿所。 如果它们在下一代垃圾回收中存活下来,它们仍然存在于年轻一代,但被认为是“中间”。 如果他们在另一个GC中幸存下来,他们就会进入老年代
在垃圾收集中有一个重要的术语:“代际假设”。 这基本上表明大多数物体都很年轻。 换句话说,从GC的角度来看,大多数对象都被分配,然后几乎立即变得无法访问。 这不仅适用于V8或JavaScript,也适用于大多数动态语言

V8中有两个垃圾收集器。 Major GC(Mark-Compact)从整个堆中收集垃圾。 Minor GC(Scavenger)在新生代收集垃圾。

Major GC(标记整理法)

标记整理法从整个堆中收集不再使用的内存

标记整理法的步骤分为三部分:
1、找到未使用的对象,进行标记
2、回收内存
3、对内存进行压缩管理

标记阶段

确定可以收集哪些对象是垃圾收集的重要部分。 垃圾收集器通过可访问性作为“活跃度”来实现此目的。 这意味着必须保留当前在运行时内可以访问的任何对象,并且收集任何无法访问的对象。

标记是找到可访问对象的过程。 垃圾收集器从一组已知的对象指针开始,称为根集。 这包括执行堆栈和全局对象。 然后它跟随每个指向JavaScript对象的指针,并将该对象标记为可访问。 垃圾收集器跟踪该对象中的每个指针,并以递归方式继续此过程,直到找到并标记了在运行时中可以访问得到的所有对象。

清除阶段

清除阶段是将不再使用的对象留下的内存空间添加到称为空闲列表(free-list)的数据结构中的过程。

标记完成后,垃圾收集器会找到不可访问的对象留下的连续内存空间,并将它们添加到相应的空闲列表中。 空闲列表由内存块的大小分隔。 在将来我们想要分配内存时,我们只需查看空闲列表并找到适当大小的内存块

压缩

标记整理法还根据碎片启发式选择撤离/压缩某些页面。我们将存活的对象复制到当前未被压缩的其他页面中(使用该页面的空闲列表)。 这样,我们可以利用不再使用的对象释放的小而分散的内存空间。

这种垃圾收集器的一个潜在弱点是,当我们分配大量长寿命对象时,我们需要花费很高的成本来复制这些对象。 所以我们选择仅压缩一些高度分散的页面,而只是对其他页面进行扫描,而不会复制存活的对象

Minor GC(Scavenger)

Major GC可以有效地从整个堆中收集垃圾,但是代际假设告诉我们新分配的对象很可能需要进行垃圾收集。

Scavenger仅收集新生代,存活的对象会被撤离到另外的页面中。 V8在新生代中采用了“半空间”设计。 这意味着总空间的一半总是空的,以允许这个撤离步骤。 在清除过程中,这个最初为空的区域被称为“To-Space”。 我们复制的区域称为“From-Space”。 在最坏的情况下,每个对象都可以在清除过程中都可以存活,我们需要复制每个对象。

清除过程中,我们有一组根指针,是旧空间中的指针,指向新生代中的对象。 我们通过维护这组指针的列表,就不用为每个清除过程去跟踪整个堆。 当堆栈和全局变量结合使用时,我们就可以知道每一代对新生代的引用,而不需要追溯到整个老年代。

撤离步骤将所有幸存的对象移动到连续的内存块(在页面内)。 这样做的好处是可以完成删除碎片 - 不可访问对象留下的空白。 然后我们切换两个空间,即To-Space变为From-Space,反之亦然。 GC完成后,新的分配将在From-Space的下一个空闲地址发生。


image.png

清理的最后一步是更新引用已移动的原始对象的指针。 每个复制的对象都会留下一个转发地址,用于更新原始指针以指向新位置。


image.png

在清理时,我们实际上执行这三个步骤 - 标记,撤离和指针更新 。所有步骤是交错进行的

Orinoco

这些算法和优化在很多垃圾收集文献中都可以了解到,并且很多垃圾收集语言中已经实现了。
测量垃圾收集所花费时间的一个重要指标是主线程在执行GC时暂停的时间。 对于传统的“全局停顿”垃圾收集器(全局JavaScript暂停去执行垃圾收集任务)来说,这个时间会累加起来,而花在GC上的时间直接降低了用户体验,包括页面质量差,渲染和延迟。

Orinoco利用并行,增量和并发技术进行垃圾收集,以释放主线程。

并行

并行是指主线程和辅助线程同时执行大致相同数量的工作。 这仍然是一种“全局停顿”(当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程)的方法,但总暂停时间现在除以参与的线程数(加上一些同步开销)。 这是三种技术中最简单的一种。因为没有JavaScript运行, JavaScript堆暂停,因此每个辅助线程只需要确保它可以同步地访问任何其他的辅助线程


image.png
增量

增量是指主线程可以间歇性地进行少量工作。 我们不会在增量暂停时执行整个GC,只会进行GC所需总工作量的一小部分。但 这更加困难,因为如果JavaScript在每个增量工作段之间执行,这意味着对象的状态已经发生改变,这可能使先前以增量方式完成的工作失效。 另外,从图中可以看出,利用增量并没有减少在主线程上花费的时间(事实上,它通常会略微增加),它只会随着时间的推移而扩展。不过,它可以解决主线程延迟的问题。 通过允许JavaScript间歇运行,同时执行垃圾收集任务,应用程序仍然可以响应用户输入并在一些动画上取得良好的效果。


将GC分割成一部分一部分进入主线程执行
并发

并发是当主线程不断执行JavaScript时,辅助线程完全在后台运行GC。 这是三种技术中最难的一种:JavaScript堆中的任何内容都可以随时更改,从而使我们之前完成的工作无效。 最重要的是,当辅助线程和主线程同时对同一对象进行读写操作,会产生读写竞赛机制。 这里的优点是主线程可以全部用于执行JavaScript - 尽管这里会有一小部分开销用于与辅助线程进行同步


image.png

当前几种垃圾收集器

Scavenging

今天,V8在新生代中的GC通过在辅助线程上使用并行清理分配工作。 每个线程接收到许多指针,它们快速将所有活动对象撤离到To-Space。 在撤离对象过程时,清理任务必须同步进行读/写/比较和交换操作; 有可能另一个清理任务通过不同的路径找到了相同的对象并移动了它。 无论哪个辅助线程成功撤离对象,都会返回并更新指针。 它留下了一个转发指针,以便访问该对象的其他worker可以在找到它们时更新其他指针。 为了快速同步地分配存活对象,清理任务使用本地线程分配缓冲区。

Major GC

V8中的主要垃圾收集器(标记整理法)以并发标记开始。 当堆动态计算受到限制时,将启动并发标记任务。辅助线程跟踪所有的指针并标记每个找到的对象。 当JavaScript在主线程上执行时,并发标记完全在后台进行。 在辅助程序执行并发标记时,写入障碍用于跟踪JavaScript创建的对象之间的引用

当并发标记完成或者动态分配受到限制时,主线程执行快速标记完成步骤。 主线程暂停在此阶段开始。 这是标记整理法的总暂停时间。 主线程再次扫描根集,以确保所有活动对象都已经标记,然后与一些辅助程序一起启动并行压缩和指针更新。主线程在暂停期间启动并发清除任务。 它们并发地运行到并行压缩任务和主线程, 即使JavaScript在主线程上运行,它们也可以继续运行。

Idle-time GC

JavaScript用户无法直接访问垃圾收集器,它在实现的时候就定义好了。 但是,V8为一些嵌入器提供了一种触发垃圾收集的机制。 GC可以发布“空闲任务”,这些是可选的,同样最终都会被触发。 像Chrome这样的嵌入器就有空闲时间的概念。 例如,在Chrome中,每秒60帧,浏览器大约有16.6毫秒来渲染动画的每一帧。 如果动画工作提前完成,Chrome可以选择在下一帧之前的空闲时间运行GC创建的其中一些空闲任务。


空闲GC利用主线程上的空闲时间主动执行GC工作

相关知识

V8中的垃圾收集器自成立以来已经走过了漫长的道路。 向现有GC添加并行,增量和并发技术是一项多年的努力,但已取得成效,将大量工作转移到后台任务。 它大大改善了暂停时间,延迟和页面加载,使动画,滚动和用户交互更加顺畅。 并行Scavenger将主线程新生代垃圾回收总时间减少了大约20%-50%,具体取决于工作负载。 空闲时间GC可以在闲置时将Gmail的JavaScript堆内存减少45%。 同时进行标记和扫描可以减少大型WebGL游戏中50%的暂停时间

大多数开发人员在开发JavaScript程序时不需要考虑GC,但了解一些内部结构可以帮助您考虑内存使用情况和有用的编程模式。 例如,对于V8堆的代际结构(新生代和老年代),从垃圾收集器的角度来看,存活期短的对象的开销是非常小的, 因为我们只需要承担在垃圾收集中存活下来的对象的开销。 这些类型的模式适用于许多垃圾收集语言,而不仅仅是JavaScript。

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

推荐阅读更多精彩内容