Fork me on GitHub

V8 内存分配与垃圾回收

V8 的垃圾回收策略主要基于分代式垃圾回收机制。所谓分代式,就是将内存空间分为新生代和老生代两种,然后采用不同的回收算法进行回收。

1、新生代空间

新生代空间中的对象为存活时间较短的对象,大多数的对象被分配在这里,这个区域很小但是垃圾回特别频繁 。

它将堆内存一分为二,每一部分空间称为 semispace,其中一个处于使用状态(from 空间),另一个处于闲置状态(to 空间)

对于新产生的对象,将从 from 空间中分配内存 。

新生代分配内存非常容易,我们只需要保存一个指向内存区的指针,不断根据新对象的大小进行递增即可。当该指针到达了新生代内存区的末尾,就会触发一次垃圾回收。

新生代的垃圾回收采用 Scavenge 算法 ,其工作原理如下:

首先检查 from 空间,将存活对象复制到 to 空间,非存活对象将会被释放。完成复制后,from 空间和 to 空间角色发生转换。新产生的对象始终从 from 空间中分配内存,to 空间则处于闲置状态。当再次进行垃圾回收时,也会执行和第一次同样的操作,如果存在以下两种情况,存活对象就会被复制到老生代空间中,这个过程称为对象晋升

  • 存活对象已经经历过一次 Scavenge 回收 。
  • to 空间内存占用比例超过 25% (保证下次新对象有足够的空间可分配)

2、老生代空间

老生代空间中的对象为存活时间长或常驻内存对象,大多数从新生代晋升的对象会被移动到这里。

老生代占用内存较多,如果使用 Scavenge算法,不仅会浪费一半空间,复制如此大块的内存消耗时间将会很长,所以 Scavenge 算法显然不适合。

V8 对于老生代中的垃圾回收,采用 Mark-Sweep (标记清除) 和 Mark-Compact(标记整理) 相结合 。

【1】Mark-Sweep

Mark-Sweep 分为 标记 和 清除 两个阶段 。

在标记阶段需要遍历堆中的所有对象,并标记那些活着的对象,然后进入清除阶段。在清除阶段,只清除没有被标记的对象。由于标记清除只清除死亡对象,而死亡对象在老生代中占用的比例很小,所以效率较高。

标记清除存在的问题是,进行一次标记清除后,内存空间往往是不连续的,会出现很多的内存碎片。如果后续需要分配一个需要内存空间较多的对象时,如果所有的内存碎片都不够用,将会使得V8无法完成这次分配,提前触发垃圾回收。

【2】Mark-Compact

标记整理正是为了解决标记清除所带来的内存碎片的问题。标记整理在标记清除的基础进行修改,将其的清除阶段变为紧缩极端。在整理的过程中,将活着的对象向内存区的一段移动,移动完成后直接清理掉边界外的内存。紧缩过程涉及对象的移动,所以效率并不是太好,但是能保证不会生成内存碎片。  

3、三种回收策略比较

从图中可以看出,在 Mark-Sweep 和 Mark-Compact 之间,由于 Mark-Compact 需要移动对象,所以它的执行速度最慢。

所以在取舍上,V8 主要使用 Mark-Sweep,在空间不足以对新生代中晋升过来的对象进行分配时才使用 Mark-Compact 。

4、垃圾回收引起的性能问题

为了避免出现 JavaScript 应用逻辑 与 垃圾回收操作 产生不一致的冲突,垃圾回收的三种基本算法都需要将应用逻辑暂停下来,待垃圾回收完成后,再恢复执行应用逻辑,这种行为被称为全停顿 。

按官方说法,以 1.5G 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要 50ms 以上,做一次非增量式垃圾回收甚至需要 1s 以上。这是垃圾回收中引起的 JavaScript 线程暂停执行时间,在这样的时间花销下,应用性能和响应能力都会直线下降。

在 V8 的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置的较小,且其中活动对象通常较少,所以即便它是全停顿,影响也不大。

但 V8 的老生代通常配置较大,且存活对象较多,全堆垃圾回收的标记、清理、整理等动作造成的停顿就会比较严重。

为降低全堆垃圾回收而导致的停顿时间,V8 做了以下改善措施:

【1】限制堆内存大小

  •  新生代:64 位系统 和 32 位系统分别为 32M 和 16 M (from 和 to 空间各占一半)
  •  老生代:64 位系统 和 32 位系统分别为 1400M 和 700 M

【2】增量式垃圾回收

V8 先从标记阶段入手,将原来一口气停顿完成的动作改为 增量标记(Incremental Marking),也就是拆分为许多小步进,每做完一步进,就让 JavaScript 应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行,直到标记阶段完成。V8 后续还引入 Lazy Sweep(延迟清除)、Incremental Compaction (增量式整理),让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行整理,进一步利用多核性能来降低每次停顿的时间。

5、垃圾回收的触发条件

  •  作用域: 能形成作用域的函数调用、with 语句 以及 全局作用域。
  •  闭包:  V8 无法主动回收内存中的闭包引用和全局变量引用。

6、内存泄漏

通常,造成内存泄漏的原因有如下几个:

  • 队列消费不及时
  • 作用域未释放

 

原创发布 @一像素 2017.08

 

参考文献:

[1]  朴灵,深入浅出Node.js,人民邮电出版社,2013

posted @ 2017-08-24 18:52 一像素 阅读(...) 评论(...) 编辑 收藏