Node.JS 内存控制

笔记部分内容摘自 《深入浅出 Node.JS》 —— 朴灵

背景

一方面由于Node基于无阻塞、事件驱动,具有内存消耗低的特点,常用于开发高网络请求的应用,因此,在服务器端,需要对资源进行高效利用。另一方面node选择了V8,享受着V8带来的良好性能与语言特性,但是也受到了V8的一些限制。
如Node通过JS生成对象,V8会在内存中回收不再被使用的垃圾内存,一方面减少了内存管理的负担,但是另一方面也造成了内存管理上的不灵活,如V8在内存上有大小限制,32位系统上限制为0.7G,64位系统为1.4G,在应用中,若不小心触碰到这个极限值,会造成进程退出,并且在进行垃圾回收的过程中,会导致阻塞代码,程序暂停。

这篇笔记,只是大致了解node里内存的控制、清理机制,具体如何使用、排查解决内存问题未做详细记录。

内存控制

内存分代

在V8中,所有的对象通过堆进行分配。

V8堆示意图

当我们声明变量并赋值时,程序会在堆中寻找空闲区分配内存,如果已经申请的堆空闲区已经不够分配新的对象,则继续申请堆内存,直到堆的大小超过V8的限制。

在V8中,将内存分为新生(年轻)代以及老生(年老)代两种,进行分别控制。

新生代:存活事件较短的对象

老生代:存活时间较长或长期驻内存的对象。

V8分代示意图

如图所示,在V8的控制下,新生内存占较少的一部分,老生空间则占据了大部分堆空间。默认情况下,新生内存空间在64位和32位系统上分别占据32MB和16MB大小,而老生内存空间在64位和32位系统上分别占据1400MB和700MB大小。

但是,V8堆内存最大保留空间的配置代码,则是如下定义的:

The young generation consists of two semi spaces and we reserve twice the amount needed for those in order to ensure the new space can be aligned to its size

由此,年轻一代的堆内存空间是两倍的配置

可以手动设置,以使用更多的内存。

1
2
3
$ node --max-old-space-size=1700 test.js //单位MB, 设置老生内存

$ node --max-new-space-size=1024 test.js //单位kb, 设置新生内存



Scavenge 算法

Scavenge 算法主要用于新生代的垃圾回收,此算法采用的是一种复制的方式实现的垃圾回收算法。

前面说了新生代的堆内存空间由两部分组成,分为正在使用中的堆内存空间,称为From空间,以及闲置的堆内存空间,称为To空间。顾名思义,Scavenge 算法正是一种在这两个空间中将仍存活的对象复制到闲置储存空间后释放原储存空间的方式。

过程:当分配对象时,显示在From空间中进行分配。当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存存活对象占用的空间会被释放,完成复制后,对换From空间和To空间,到此垃圾回收完成。

值得注意的是,如果一个对象经过多次复制依然存活,则会将此对象生存周期较长的对象,并移动到老生堆空间中,以采用其他算法进行管理,此过程称为对象的晋升。另外,当To空间使用占比已经超过限制,也会将新代对象直接移动到老生对象空间。

由于 Scavenge 算法将内存对象在两个空间中复制交换,因此,只能使用堆内存的一半空间,以牺牲空间换效率。 此算法也正因此,适合应用在新生代中,因为新生代的生命周期段,占用空间也较少。



Mark-Sweep & Mark-Compact

大致了解了新生代的垃圾回收算法,下面了解下老生代的垃圾回收机制。


Mark-Sweep

标记清除算法,通过一次遍历老生代对象,对所有还活着的对象进行标记,在遍历完成后实施清除,将所有未标记的对象清除。

但是这种算法的问题也是显而易见的,在一次操作后,会造成内存空间的不连续状态,也就是内存碎片。这种情况下,当有一个稍大的对象要放入老生代空间中时,可能由于空间过于碎片化,而导致无法找到一块适合其大小的空间存放,便会造成又一次的垃圾回收,以期待回收出一块适合的区域存放该对象。由此,提出里另一个算法 Mark-Compact。


Mark-Compact

此算法是在 Mark-Sweep 的基础上改进而来的,差别在于对象在标记为死亡后,在整理的过程中,将或者的对象向一端移动,移动完成后,直接清理掉边界外的内存,这样就不会出现内存碎片的问题了。

在V8中,这两种回收策略并不是单独使用的,而是结合使用的

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


Incremental Marking

增量标记

为了避免出现 JS 应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的 3 种基本算法都需要将应用逻辑暂停下来,这种暂停成为 “全停顿”。V8 老生代通常配置得很大,且存活对象较多,全堆垃圾回收的各种动作造成的停顿会比较可怕,需要设法改善。V8 先从标记阶段入手,将原本一口气停顿完成的动作改为增量标记,也就是拆分为许多小 “步进”,没做完一步就让 JS 应用逻辑执行一小会儿。这种垃圾回收与应用逻辑交替执行直到标记完成,可以让最大停顿时间减少 5 倍左右。

关于使用内存

参考《深入浅出NodeJS》一书的相关内容,主要说明了作用域及闭包等的使用,我觉得这些问题不如直接使用ES6的相关特性解决更加简单有效,还不容易让自己出错。所以这里就不再做繁琐的记录了。

关注内存与缓存

勿将内存当做缓存使用。

  1. 缓存限制(可使用现有的相关模块)、提供清空缓存队列的接口。
  2. 使用redis、Memcached

内存泄漏排查

使用相关模块:node-heapdump/node-memwatch

说实话,我感觉以我现在的能力,我觉得我根本不会考虑到内存泄漏、检查等之类的问题→_→
所以暂时简单记录,以后如果需要的话,再详细补充