内存限制

在一般的后端开发语言,在基本的内存使用上是没有限制的。

主要是因为 Node 基于 V8构建,所以在 Node 中使用的 JavaScript 对象基本是通过 V8 自己的方式来分配和管理的。

V8 的内存管理机制在浏览器上使用是完全足够的,但是对服务端来讲,限制了开发者随心所欲使用内存的想法。

尽管我们很少的场景会使用到很大的内存,比如大文件的读取我们都可以通过 Stream 来完成,但 Node 还是提供了方法打开这个限制。

默认内存大小

  • 64位系统下,约为 1.4 GB
  • 32位系统下,约为 0.7 GB

查询内存信息

1
2
3
4
5
6
7
8
9
10
11
import { memoryUsage } from 'process'

console.log(memoryUsage())
// 打印:
// {
// rss: 4935680,
// heapTotal: 1826816,
// heapUsed: 650472,
// external: 49879,
// arrayBuffers: 9386
// }
  • heapTotalheapUsed 指的是 V8 的内存使用量。
  • external 指的是绑定到 V8 管理的 JavaScript 对象的 C++ 对象的内存使用量。
  • rss,常驻集大小,是进程在主内存设备(即总分配内存的子集)中占用的空间量,包括所有 C++ 和 JavaScript 对象和代码。
  • arrayBuffers 是指为 ArrayBufferSharedArrayBuffer 分配的内存,包括所有 Node.js Buffer。 这也包含在 external 值中。 当 Node.js 被用作嵌入式库时,此值可能为 0,因为在这种情况下可能不会跟踪 ArrayBuffer 的分配。

修改内存限制

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

修改新生代空间的参数好像改成了:

1
node --max-semi-space-size=1025 test.js

可以使用:node --v8-options | greap max 来查看

垃圾回收机制

V8 使用了内存分代的方式来管理内存。

新生代中的对象为存活时间较短的对象;而老生代中的对象为存活时间较长或常驻内存的对象。

新生代空间

新生代空间主要使用 Scavenge 算法进行垃圾回收。在 Scavenge 的实现上 主要使用 Cheney 算法。

Scavenge 是一种复制算法,新生代空间会被一分为二划分成两个相等大小的 from-space 和 to-space。

它的工作方式是将 from space 中存活的对象复制出来,然后移动它们到 to space 中或者被提升到老生代空间中,对于 from space 中没有存活的对象将会被释放。完成这些复制后在将 from space 和 to space 进行互换。

缺点:

  • 只能使用堆内存的一半

优点:

  • 由于新生代空间,大部分都是存活时间短的对象,所以只复制存活对象,效率比较高。

晋升

对象从新生代空间移动到老生代空间的过程称为晋升

主要晋升条件有两个:

  • 对象是否经历过 Scavenge 回收
  • To 空间的内存占用比是否超过 25%

老生代空间

老生代空间的对象,由于存活时间比较长,存活对象占较大比重。

所以无法采用Scavenge算法:

  • 存活对象多导致复制效率低
  • 浪费一半内存,对于存活对象较多的老生代空间是无法接受的

所以在老生代空间使用 Mark-Sweep 和 Mark-compact 相结合的方式进行垃圾回收

Mark-Sweep

标记清除算法分为标记和清除两个阶段。

Mark-Sweep在标记阶段遍历堆中所有对象,并标记或者的对象,在随后的清除阶段,只清除没有标记的对象。

该算法最大的问题是,在清除后,内存空间会出现不连续的状态,如果后续有一个大对象,那么这些碎片空间都无法完成这次分配,导致提前触发垃圾回收。

Mark-compact

和Mark-Sweep的差别是,Mark-compact标记完成后,在整理过程中会将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

STW

stop-the-world,垃圾回收的三种基本算法,都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为全停顿(stop-the-world)

V8通过引入增量标记,来减少全停顿的时间

垃圾回收机制性能优化

增量标记

Incremental marking

为了降低全堆垃圾回收的停顿时间,增量标记将原本的标记全堆对象拆分为一个一个任务,让其穿插在JavaScript应用逻辑之间执行,它允许堆的标记时的5~10ms的停顿。

增量标记在堆的大小达到一定的阈值时启用,启用之后每当一定量的内存分配后,脚本的执行就会停顿并进行一次增量标记。

懒性清理

Lazy sweeping

增量标记只是对活动对象和非活动对象进行标记,惰性清理用来真正的清理释放内存。

当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理的过程延迟一下,让JavaScript逻辑代码先执行,也无需一次性清理完所有非活动对象内存,垃圾回收器会按需逐一进行清理,直到所有的页都清理完毕。

增量标记与惰性清理的出现,使得主线程的最大停顿时间减少了80%

缺点:

  • 并没有减少主线程的总暂停的时间,甚至会略微增加
  • 由于写屏障(Write-barrier)机制的成本,增量标记可能会降低应用程序的吞吐量

并发

Concurrent

并发式GC允许在在垃圾回收的同时不需要将主线程挂起,两者可以同时进行,只有在个别时候需要短暂停下来让垃圾回收器做一些特殊的操作。

但是这种方式也要面对增量回收的问题,就是在垃圾回收过程中,由于JavaScript代码在执行,堆中的对象的引用关系随时可能会变化,所以也要进行写屏障操作。

并行

Parallel

并行式GC允许主线程和辅助线程同时执行同样的GC工作,这样可以让辅助线程来分担主线程的GC工作,使得垃圾回收所耗费的时间等于总时间除以参与的线程数量(加上一些同步开销)。