- 新生代:大部分新对象都在这里
- 老生代:
- 对象的布局结构信息在
Map Space
分配 - 编译出来的代码在
Code Space
分配 - 太大不能直接放进来的对象在
Large Object Space
分配 - 创建的对象常常被晋升到
Old Space
的函数,在这些对象达到一定的生存率(survival rate
)之后它再创建的对象会被自动在Old Space
分配(pretenuring
) - 由新生代晋升(
Promotion
)而来
- 对象的布局结构信息在
由C++直接分配内存,如Buffer
。参照后续有关Buffer和Stream
的小节。
V8的新生代和老生代空间都是分页的(关于分页机制参考上一节的内存管理)。新生代采用连续的分页,而老生代采用离散的通过链表连接的分页。
图片来源:alinode
GC的动机来源:
- 分配新内存的需求(已经分配的内存无法满足新的应用内存需求)触发GC
- 当内存使用率达到一定阀值时触发
- 周期性触发
上图中,GC操作和其他任务执行处于同一线程,如果GC阶段比较长,那么会长时间阻塞其他任务的执行。Chrome
浏览器有一个叫做Task Scheduler
的调度器,在浏览器处于闲置状态期间执行GC:
此种GC执行机制称为stop the world
,此种处理方式的好处是简单易控制,不会出现GC任务与其他任务同时操作相同对象的情况。但是这会导致用户体验问题,由于GC时期过长导致动画更新频率低于60 FPS
就是其影响之一。我们要做的是尽量减少stop the world
的时间。
关于Jank
指标: https://www.chromium.org/developers/design-documents/rendering-benchmarks
参考: https://v8project.blogspot.jp/2015/10/jank-busters-part-one.html
代号为Orinoco
的任务主要是实现V8的并行、并发GC,减少Jank
来提高吞吐量。V8采用分代的GC机制,在GC过程中存在大量的对象位置移动以及位置移动后指向它们的指针的引用迁移过程,还有老生代空间的紧缩操作等等。在没有Orinoco
之前,它们的执行情况如下:
可见GC过程会比较复杂,这种执行方式很容易造成严重的Jank
。
新生代对象的移动(复制)和老生代的内存紧凑过程都实现了页级别的并行优化,然后新生代和老生代的操作互不影响,同样也是并行的:
当对象从某个位置移动到新的位置时,指向它的指针们的指向也要发生变化,如果使用遍历堆的方法找到所有的指针效率很低下,所以V8使用remembered set
来记录那些可能会产生变化的指针(intersting
指针),那些指向新生代对象的指针,以及指向处于碎片化的老生代区某个对象的指针都会被记录。在之前的V8版本中,记录方式如图:
通过数组或者store buffers
的形式记录,且采用write barrier
机制。
此种方式可能导致remembered set
里存在重复的记录,会导致对指针操作的优化操作(如并行处理)失效,Orinoco
采用下图所示的记录方式:
black allocation
将所有新出现在 Old Space
的对象(包括pretentured
的分配或者晋升)直接标记为黑色,放在特殊的内存页(black page
)中,这个内存页里只有black objects
,因此一定能活过下一次 GC。这样做可以一定程度上减轻 marking 的负担,即使猜错了,下下轮 marking 前这些对象又会先刷白,只逃过一次 GC 所以造成的影响也不大。
Scavenge
的基本思想是用空间换时间,将新生代空间平分成两个semispace
,live
对象和死亡对象在它们之间不断交换迁移:
新生代的回收过程一般为stop the world
在这个过程中会有上面提到的优化机制:对象复制迁移的页级别并行和指针指向迁移的并行优化。
此算法不用担心有内存碎片的情况,由于存活的对象比较少,因此stop the world
的过程比较短。但是不适合大内存对象和老生代对象。
在New Space
里存活两轮时,就会晋升到Old Space
。
老生代的回收过程分为两个部分:标记和清除(紧凑)。
标记过程负责标注出哪些对象是死亡对象将要被回收,
紧凑过程也是一个stop the world
的过程。紧凑的过程会用到上面提到的优化机制:对象复制迁移的页级别并行和指针指向迁移的并行优化。
标记死亡对象过程会阻塞主线程,如果采用一次性stop the world
方式会增加Jank
,可以采用增量方式进行,分解成多次stop the world
过程。
当标记完成后,不立即执行sweep
操作,即lazy sweep
,然后由于要执行sweep
操作的对象是死亡对象,确认不会被主线程其他任务访问,所以此时sweep
操作可以和其他任务并发执行(因为有少量同步过程,所以不叫做并行),即concurrent sweeping
,然后可以使用多个线程同时进行sweep
即为parallel sweeping
。
这样,整个V8的垃圾回收执行情况如下:
// todo