在 node.js 中,内存主要分为两个部分,堆内存和栈内存
- 堆内存(heap):存放对象和闭包上下文,v8 使用垃圾回收机制管理堆内存
- 栈内存(stack):存放局部变量,栈内存的分配比较简单,当程序离开某作用域后,其栈指针下移(回退),整个作用域的局部变量都会出栈被回收。
可通过 process.memoryUsage() 查看内存使用情况,单位为字节
- rss 是分配的整体物理内存,包括堆、栈、代码段
- heapTotal 是整体堆内存
- heapUsed 是被使用的堆内存
- external: 代表 v8 管理的绑定到 javascript 的 c ++ 对象的内存
通常情况下,v8 对堆内存的分配为,64 位系统下 1.4GB,32 位系统下 0.7GB
node 将堆内存分为新生代和老生代
- 新生代:存放存活时间较短的对象
- 老生代:存放存活时间较长的对象
在分代的基础上,对于新生代使用 Scavenge 算法,老生代使用 Mark-Sweep 和 Mark-Compact
- 对象已经经历过一次复制翻转操作。
- To 空间使用比例超过 25%,设置 25% 的比例是因为,To 空间将变成 From 空间,如果使用比例过高,可能影响后续的内存分配。
Scavenge 算法
Scavenge 算法主要通过Cheney 算法 实现,Cheney 算法将新生代分为两部分,叫做SemiSpace 空间,该两个 SemiSpace 空间分别叫做From 空间和 To 空间。
From 空间为使用空间,To 空间为闲置空间。分配新的内存空间时会分配到 From 空间。开始进行垃圾回收时,会检查 From 空间内的对象,将存活对象复制至 To 空间,释放 From 空间,然后将 From 空间和 To 空间进行翻转。
Scavenge 算法的优点是速度快,无内存碎片,缺点是,只能使用一般的内存空间,但是由于新生代都是存活时间较短的对象,需要复制的对象属于少数。属于使用空间换时间的典型算法。只适用于新生代垃圾回收策略。
在新生代回收阶段,如果存活对象满足以下两个条件,会将该对象移动至老生代,该阶段叫做晋升
- 对象已经经历过一次复制翻转操作。
- To 空间使用比例超过 25%,设置 25% 的比例是因为,To 空间将变成 From 空间,如果使用比例过高,可能影响后续的内存分配。
Mark-Sweep(标记清除)和 Mark-Compact(标记整理)
Mark-Sweep 分为标记和清除两个阶段,在标记阶段遍历所有老生代对象,对还存活的对象进行标记,在清除阶段释放未被标记的内存。
Mark-Sweep 最大问题是,经过清除阶段后,内存空间会出现不连续状态,影响以后的内存分配。可能出现一个对象,需要分配内存空间很大,而现有内存碎片不能完成分配,进行提前垃圾回收,该回收是没有必要的。
为了解决 Mark-Sweep 的内存碎片问题,Mark-Compact 被提了出来,它主要是在标记之后,将存活的对象往一侧移动,完成移动后,直接释放边界外的内存。
Incremental Marking
该三种算法执行时,为了保证 javascript 应用逻辑与垃圾回收机制保持一致,垃圾回收时都需要将应用逻辑停止,这种情况称之为“全停顿 ”
新生代 Scavange 算法,由于分配内存空间较小,存活对象较少,全停顿影响较少。
但是老生代内存空间较大,存储对象较多,全停顿时间较长。所以 V8 为了减少停顿空间,从标记阶段开始,改为增量标记(Incremental Marking)。将整体拆分为小“步进”,没执行一个“步进”就让 js 应用逻辑执行一会儿,垃圾回收与应用逻辑交替进行。