Skip to content

V8 - Orinoco(垃圾回收) #10

@Hazlank

Description

@Hazlank

V8的垃圾回收器经历过很多变化,目前这个版本的GC叫Orinoco,它由两个GC组成,并且采用增量,并发和并行的方式运行

首先,任何垃圾收集器都有一些必须定期执行的基本任务:

  1. 识别不被需要的死对象和被引用的活对象
  2. 回收/重用死对象占用的内存
  3. 使用压缩(Compact)整理碎片化内存(可选)

这些任务可以按顺序执行,也可以任意交错执行。

主GC (Full Mark-Compact)

主 GC 从整个堆中收集垃圾。
img
Major GC的三个阶段:标记、清除和压缩。

标记

弄清楚哪些对象是"死",哪些是"活",可以通过方法给这些对象打上标记。通常垃圾回收器会把所有的堆节点视为可达图来维护,从根节点(全局对象,执行堆栈等)的指针出发,跟随每个引用了当前根节点对象的指针,并将该对象标记为可达。这个过程是递归的,所以会找到并标记运行时中可到达的每个对象。

清除

标记工作完成后,不可达节点被视为垃圾,需要被回收。回收的时候,会将这些内存释放并添加到适当的空闲列表中。将来想要分配内存时,只需查看空闲列表并找到适当大小的内存块就行了,这样方便也加快了后续的空闲内存的分配。

压缩/整理

主GC还会基于碎片启发式选择疏散/压缩某些页面,像是旧 PC 上的硬盘碎片整理。当应用初始化了一大块内存,并且回收很多不存活的对象时,内存是碎片化的,死对象会在内存里留下小而分散的间隙,对于变量的查找速度会变慢。压缩的工作会将幸存的对象复制到当前未压缩的其他页面中(使用该页面的空闲列表)来解决这些问题。当然,复制幸存的,大量长寿命的对象会有高昂的代价。所以压缩的工作只针对于高度碎片化的页面,而只对其他页面进行清除

分代堆布局

V8中的堆被分成不同的区域,称为代(generations);这两块区域分别称之为老生代(old generation)和新生代(young generation),新生代又进一步分为 ‘nursery’ 子代和 ‘intermediate’

img
V8 堆被分成几代。当对象在 GC 中幸存下来时,它们移动到下一代。

对象首先分配到新生代里的nursery。如果在下一次 GC 中存活下来,它们会留在年轻代中,并被移动到intermediate。如果又再另一次 GC 中幸存下来,它们就会被移到老年代。

对于一些经常在GC中存活下来的变量,它可能是会被一直用到直到程序结束时才会回收,可以减少对它进行GC的次数,去重复的进行标记,清除等的工作。大量的GC活动应该留给nursery堆里的变量。并且存活下来的很小一部分对象能够复制压缩至其他页面。

次GC(Scavenger)

除了主GC,还有个次要的GC,次GC主要从新生代中回收垃圾。

只在年轻代内收集的 Scavenger 中,幸存的对象总是被疏散到一个新的页面。 V8为young generation采用了“半空间”设计,意味着总空间的一半总是空的。新加入的对象会放在一半的区域里,次GC进行标记清除后,会把剩下存活的对象放在另一半的空间里。存活下来需要被复制的区域称为“From-Space”,最初的空白区域被称为“To-Space”。在最坏的情况下,每个对象都可以幸存下来,需要复制每个对象。

这个过程相当于是完成了内存整理的工作,复制到"To-Space"为连续的内存块,就不会有内存碎片的问题了。
img

经过次GC的活动,young generation的空间会被耗完。所以第二次GC存活下来的应该疏散到old generation。它会更新已经移动到intermediate的原始对象的指针并指向新的Old generation的位置。
img

GC执行工作

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

并行

并行是主线程和辅助线程同时执行大致相等的工作。这是三种技术中最简单的一种,由于JavaScript堆被暂停,没有 JavaScript 运行,因此每个辅助线程只需要确保由于在分代堆布局时移动导致数据地址发生了改变,需要同步跟新引用这些对象的指针的问题

img
主线程和辅助线程同时处理同一个任务。

增量

增量垃圾回收就是垃圾收集器将工作分成更小的块穿插在主线程的不同任务之间执行,它不会做很多的工作,不然会阻碍js运行。当然,如果在标记后js的对象发生了改变,之前的标记就是无用功了,需要重新进行标记

img
GC 任务的小块被交错到主线程执行中。

并发

并发是多开几个辅助线程完全在后台做 GC 工作,不会中断主线程执行JavaScript。这是三种技术中最困难的,因为JavaScript堆上的任何内容都可以随时更改,会使之前所做的工作无效。最重要的是,有读/写竞争需要担心,因为辅助线程和主线程有可能同时读取或修改相同的对象。优点是主线程可以完全自由地执行JavaScript。

img
GC 任务完全在后台发生。主线程可以自由运行 JavaScript。

引用: https://v8.dev/blog/trash-talk

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions