V8的内存限制

在一般的后端语言中,在内存的使用上没什么限制,而在Node.js中却是被限制的(64ͮ位系统下约为1.4 GB,32ͮ位系统下约为0.7 GB)。这就导致Node.js无法直接操作过大的内存操作。

造成这种问题的原因,一是V8本是为浏览器设计的,很少会遇到使用大量内存的场景。二是垃圾回收机制所限,以1.5GB的垃圾回收堆内存为例,V8做一次小的垃圾回收就需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上,这是前端浏览器与后端服务器都无法接受的。

当然这个限制也是可以修改的,在启动的时候传入以下参数:

1
2
node --max-old-space-size=1700 test.js // 单位为MB
node --max-new-space-size=1024 test.js // 单位为KB

max-old-space-size老生代内存空间,主要存放存活时间较长或常驻内存对象
max-new-space-size新生代内存空间,主要存放存活时间较短的对象
这两个参数在V8初始化时生效,一旦生效不可改变,这意味着V8使用的内存无法根据使用情况自动扩充。

补充:

默认情况下

老生代内存空间 在64位系统上为1464MB,32位系统上为732MB

新生代内存空间 在64位系统上为32MB,32位系统上为16MB

如果想要跳出V8对内存大小的限制,我们可以使用Buffer,它不经过V8的内存分配机制,所以不存在堆内存的大小限制。

由于V8的内存限制,我们无法通过fs.readFile()和fs.writeFile()直接进行大文件操作,而改用fs.createReadStream()和fs.createWriteStream()方法以流的形式实现大文件操作。

1
2
3
4
5
6
7
8
9
10
var reader = fs.createReadStream('in.txt')
var writer = fs.createWriteStream('out.txt')
reader.on('data', function (chunk) {
writer.write(chunk)
})
reader.on('end', function () {
// 或者使用更简洁的方式:
var reader = fs.createReadStream('in.txt')
var writer = fs.createWriteStream('out.txt')
reader.pipe(writer)

新生代垃圾回收

新生代内存中主要通过 Scavenge 算法(直译捡垃圾吃的算法,手动滑稽)进行, Scavenge 算法具体又使用到 Cheney 算法。其将堆内存一分为二,每一部分空间称为 semispace。这两个空间一个处于使用中(我们称为FROM空间),另一个处于闲置状态(称为TO空间)。分配对象时是在FROM空间中进行分配。

这张是我画的整个过程:

V8新生代垃圾回收机制

Scavenge 算法的缺点是只使用了堆内存中的一半,由于该算法只复制存活对象,且存活时间短的对象很少,所以Scavenge 算法在时间效率上表现优异,正式因为这些特点,所以这个算法很适合新生代垃圾回收。

新生代对象晋升

当一个新生代中的对象经过多次新生代回收后任然存活,它将会被认为是生命周期较长的对象,这种对象随后将会被移动到老生代中,这叫做新生代对象晋升

除此之外如果从FROM复制对象到TO空间时,TO空间的内存占比已经超过了25%,那么这个对象将直接晋升到老生代空间中

设置25%这个值是因为 Scavenge 回收完成后,当前TO空间会变为FROM空间,如果其使用率过高,会影响后续内存分配。

附两张新生代晋升判断示意图:

新生代晋升判断示意图1

新生代晋升判断示意图2

老生代垃圾回收

老生代中使用了 Mark-SweepMark-Compact,来进行垃圾回收。

不使用 Scavenge 有两个原因,一是老生代中对象较多,复制效率会很低,二是老生代比较大,使用 Scavenge 会浪费一半内存空间,这一半就太多了。

Mark-Sweep分为两个阶段标记清除标记阶段会遍历堆内存中所有对象,并标记所有活着的对象。在之后的清除阶段将没有被标记的对象释放。

但这样Mark-Sweep会造成内存不连续,带来了内存碎片问题,因此引入了 Mark-CompactMark-CompactMark-Sweep基础上演变而来,它在整理的时候会将活着的对象往一端移动,整理与移动完成之后,直接清掉边界外的内存,完成回收。

Mark-Sweep的清理过程

Mark-Sweep

但Mark-Sweep带来了内存碎片问题,Mark-Compact就是用来解决这个问题的

Mark-Compact

在V8中两种回收策略是结合使用的Mark-Compact要移动对象,速度不快,故V8优先使用Mark-Sweep,在空间不足以分配给晋升对象时才使用Mark-Compact

垃圾回收时的增量标记(Incremental Marking)

垃圾回收执行时,代码运行是完全暂停的,在垃圾回收后,才会继续执行,这种行为被称为全停顿

由于新生代内存空间不大,清理较快,即使全停顿对代码运行影响也不大。

老生代就不能这样了,老生代通常都比较大,代码如果全停顿下来等待老生代垃圾回收完成就很可怕了。所以老生代采取的是垃圾回收与逻辑代码交替执行,清理一小段,就让代码执行一小会儿,这个交替执行直到标记阶段完成。

…未完

参考:

《深入浅出Node.js》