本文是V8引擎详解系列的第七篇,重点内容是关于V8的垃圾回收机制,以及V8对垃圾回收的优化策略,本文首先须要对内存结构有一个初步了解,不了解的能够先看一下V8引擎详解(六)——内存结构。 文末会有已经完成的系列文章的连接,本系列文章还在不断更新欢迎持续关注。javascript
咱们先简单了解一下垃圾回收的概念,好比V8引擎在执行代码的过程当中遇到了一个函数,那么咱们会建立一个函数执行上下文环境并添加到 调用栈 顶部,函数的做用域里面包含了函数中全部的变量信息,在执行过程当中咱们分配内存建立这些变量,当函数执行完毕后函数做用域会被销毁,那么这个做用域包含的变量也就失去了做用,那么销毁它们回收内存的过程,咱们就叫作垃圾回收。java
垃圾回收的过程是v8引擎自动帮咱们执行的,在绝大部分状况下v8都能很好的完成这个过程,可是做为一段程序,能帮咱们cover住的状况是有限的,因此一旦咱们代码不够严谨,就会引起内存泄露。算法
你们都知道javascript语言的一个特色就是单线程,单线程意味着执行的代码都是按顺序执行的且同一时间也只能处理一个任务,那么V8在执行垃圾回收任务的时候,其余的任务都将处于等待状态,直到垃圾回收任务结束后才能执行其余任务,若是垃圾回收任务的执行时间过长就不可避免的对用户体验形成影响,V8为了减小这种影响也作了一系列的优化,咱们一块儿来看一下V8究竟是如何作垃圾回收的而且是如何优化的。编程
代际假说(The Generational Hypothesis)是垃圾回收领域中的一个重要术语, V8的垃圾回收的策略也是创建在该假说的基础之上。
代际假说也很简单,主要有两个特色:浏览器
基于这个这个假说 V8 才会把堆分为新生代和老生代两个区域,同时设计了两个垃圾回收器:缓存
(新生代和老生代已经在做者以前的文章介绍过,不了解的能够看以前的文章)bash
副垃圾回收器主要用来回收新生代的垃圾,一般咱们新建立的对象都会先分配到新生代内存区中。
新生代内存区会分红两个部分(space),from space 和 to space , 这两个区域本质都是同样的,都拥有两个状态 工做状态 和 空闲状态且当一个为工做状态的时候另外一个必定是空闲状态。并发
好比咱们新建立一个对象:编程语言
以上就是所谓的置换也能够说是翻转过程,由于这种复制操做须要时间成本,因此新生代的空间每每并不大,因此执行的也较为频繁。函数
随着程序的运行,某些对象一直在被使用会持续的积压在新生代区域,为了解决这个问题,V8采用了 晋升机制 将知足条件的对象放到老生代内存区中存储,释放新生代内存区域的空间。
晋升机制的条件:
晋升后的对象分配到老生代内存区,便由老生代内存区来管理。
主垃圾回收器主要用来回收老生代的垃圾,一般会有在新生代晋升后的对象以及初始占用空间就很大的对象会存储在老生代内存区。
主垃圾回收器采用的方法和次垃圾回收器的方法彻底不一样,主垃圾回收器会先使用标记 - 清除(Mark-Sweep)的算法进行垃圾回收。
引用一下李兵老师的描述:
首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程当中,能到达的元素称为活动对象,没有到达的元素就能够判断为垃圾数据。
接下来就是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程彻底不一样,主垃圾回收器会直接将标记为垃圾的数据清理掉。
可是咱们经过这种标记清除的方式进行内存清理会产生大量不连续的内存碎片,当咱们想要存储一个大的对象的时候就可能没有足够的空间,那么除了执行 标记 - 清除(Mark-Sweep) 算法外,还经过 标记 - 整理(Mark-Compact) 算法进行垃圾回收。
标记 - 整理(Mark-Compact) 算法主要也是分两步:
V8经过标记 - 清除(Mark-Sweep) 以及 标记 - 整理(Mark-Compact) 两种算法对老生代内存区进行垃圾回收,这就是主垃圾回收器的主要工做。
上文中描述的V8的两个垃圾回收器所采用的方法其实在具备垃圾回收机制的编程语言中都是很是常见的。
评价一个垃圾回收机制好坏的一个重要标准是取决于执行垃圾回收时主线程挂起的时间,而V8为了优化这一部分体验(减小主线程挂起的时间),启动代号为Orinoco的垃圾回收器项目来专门进行垃圾回收策略的优化。
Orinoco共实现了三个优化
先说第一个优化 并行垃圾回收,咱们以前提到过新生代内存区 和 老生代内存区根据以前讲过的垃圾回收机制,咱们能够肯定在新生代内存区中的对象和老生代内存区中的对象是彻底不一样的,那么也就是说新生代在执行 标记->复制->清理 的操做和老生代执行 标记->清理->紧凑 的操做是没有任何依赖关系的。
因而Orinoco判断将没有依赖关系的垃圾清理逻辑(不止上述一种)经过并行执行的方式来优化减小执行垃圾回收占用主进程的时间。因此Orinoco只须要开启辅助几个辅助进程就能够同时完成垃圾清理的工做以下图:
(图片来源:v8.dev/blog/trash-…)
第二个优化 增量垃圾回收, 虽然并行垃圾回收的并行机制能够有效的减小主进程的占用,可是面对一个大的对象一次执行标记也要话很长的时间,从2011年开始V8引入了增量标记机制,也就是增量垃圾回收机制。
将一次大的任务分解为更小的块,容许应用程序在块之间运行。
这种优化对于标记的实现带来了很大的挑战,如何保存当时的扫描结果?标记好的数据若是被主线程修改了,如何正确的处理?
因而V8采用了 标记位 和 标记工做表 来实现标记。
标记位用来标记三种颜色:白色(00)、灰色(10)、黑色(11),
整个过程如图:
从根节点开始标记
遍历处理
完成后的最终形态
这个过程是否是有点绕,那我举个例子(不知道恰不恰当哈)
好比有一个小偷团伙
那回到以前的问题,标记好的数据若是被主线程修改了,如何正确的处理? V8 使用了写屏障(write-barrier) 机制来实现,这个机制也不难理解,简单来讲就是强制让黑色的对象不能直接指向白色的对象。 好比咱们执行一个写入操做:
// 调用 `object.field = value` 以后
write_barrier(object, field_offset, value) {
if (color(object) == black && color(value) == white) {
set_color(value, grey);
marking_worklist.push(value);
}
}
复制代码
将新写入的对象从初始的白色直接变为灰色,那么 标记工做表 就没有空,那么就继续执行标记的过程,保证了正确的标记数据。
并发垃圾回收和并行垃圾回收是彻底不一样的用一张图来表示
并行垃圾回收发生在主线程和工做线程上。应用程序在整个并行标记阶段暂停。
并发垃圾回收主要发生在工做线程上。当并发垃圾回收正在进行时,应用程序能够继续运行。
一般以上三种方式也不是单独存在的,而是聚合在一块儿使用具体以下图:
空闲时垃圾回收并不属于Orinoco项目,是V8实现的一种优化策略。
一般调度程序经过对任务队列占用率的了解,以及和V8其余组件接收到的信号,使它能够估计V8什么时候处于空闲状态,以及可能保持多长时间。利用这个信息,V8能够分配一些优先级不高的垃圾回收任务在这个空闲时间去作。
好比V8会使用Chrome浏览器的task scheduler , 根据从Chrome其余各类组件接收到的信号以及旨在估算用户意图的各类启发式方法,动态地从新分配任务的优先级。例如,若是用户触摸屏幕,则调度程序将在100毫秒的时间段内优先处理屏幕渲染和输入任务,以确保用户界面在用户与网页交互时保持响应。
例如,若是以60 FPS进行渲染,则帧间间隔为16.6 ms。若是没有在屏幕上进行任何有效的更新,则task scheduler 将启动更长的空闲时间,该空闲时间持续到启动下一个待处理任务为止,且上限为50毫秒,以确保Chrome保持对意外用户输入的响应。
更细节的关于空闲任务的描述能够看 queue.acm.org/detail.cfm?… 这篇文章,本文很少赘述了。
本文主要了解了V8的垃圾回收机制以及采用的一些优化方法,垃圾回收机制相对比较简单,可是Orinoco优化的方法相对比较难以理解(做者尚未彻底理解并发垃圾回收究竟是如何作的因此没有深刻的写,后面理解清楚会更新),若是有什么错误,请在评论中和做者一块儿讨论,若是您以为本文对您有帮助请帮忙点个赞,感激涕零。
queue.acm.org/detail.cfm?…
time.geekbang.org/column/arti…
v8.dev/blog/concur…
v8.js.cn/blog/orinoc…
V8引擎详解(一)——概述
V8引擎详解(二)——AST
V8引擎详解(三)——从字节码看V8的演变
V8引擎详解(四)——字节码是如何执行的
V8引擎详解(五)——内联缓存
V8引擎详解(六)——内存结构
V8引擎详解(七)——垃圾回收机制