简单介绍Node.js的内存分配和垃圾回收
内存分配
Node.js是一个由JavaScript V8引擎控制的C++程序
V8的内存管理模式一个运行的程序通常是通过在内存中分配一部分空间来表示的。这部分空间被称为驻留集(Resident Set)。
V8的内存管理模式有点类似于Java虚拟机(JVM),它会将内存进行分段:
- 代码 Code:实际被执行的代码
- 栈 Stack:包括所有的携带指针引用堆上对象的值类型(原始类型,例如整型和布尔),以及定义程序控制流的指针。
- 堆 Heap:用于保存引用类型(包括对象、字符串和闭包)的内存段
查看内存使用情况:
|
|
垃圾回收机制
V8的垃圾回收机制分代式垃圾回收,基于这个机制,V8把内存分为新生代(New Space)和 老生代 (Old Space)。
新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
–max-old-space-size 命令就是设置老生代内存空间的最大值,而 –max-new-space-size 命令则可以设置新生代内存空间的大小。
垃圾回收算法
在V8中,按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同的内存施以更高效的算法。
新生代中的垃圾回收在新生代中,主要通过 Scavenge 算法进行垃圾回收。
Scavenge
Scavenge算法将堆内存一分为二,每一部分空间称为semispace。在这两个semispace空间中,只有一个处于使用中,另外一个处于闲置状态。
处于使用状态的semispace称为From空间,处于闲置状态的semispace称为To空间。当我们分配对象时,先是从From空间中分配。当开始进行垃圾回收时,
会检查From空间中存活的对象,这些存活的对象会被复制到To空间中,而非存活的对象占用的空间会被释放。完成复制后,From空间和To空间角色互换。
在垃圾回收的过程中,就是通过将存活对象在两个semispace空间之间进行复制。
新生代中的对象怎样到老生代中
在新生代存活周期长的对象会被移动到老生代中,需要符合两个条件中的一个:
- 对象是否经历过Scavenge回收。
对象从From空间中复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收,如果已经经历过了,则将该对象从From空间中复制到老生代空间中。 - To空间的内存占比超过25%限制。
当对象从From空间复制到To空间时,如果To空间已经使用超过25%,则这个对象直接复制到老生代中。这么做的原因在于这次Scavenge回收完成后,这个To空间会变成From空间,
接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
所以,V8在老生代中主要采用「Mark-Sweep」算法与「Mark-Compact」算法相结合的方式进行垃圾回收。
老生代中的垃圾回收对于老生代的对象,由于存活对象占比较大比重,
使用Scavenge算法显然不科学。一来复制的对象太多会导致效率问题,二来需要浪费多一倍的空间。
Mark-Sweep
Mark-Sweep是标记清除的意思,分为标记和清除两个阶段。在标记阶段遍历堆中的所有对象,并标记存活的对象,在随后的清除阶段中,只清除标记之外的对象。
但是Mark-Sweep有一个很严重的问题,就是进行一次标记清除回收之后,内存会变得碎片化。如果需要分配一个大对象,这时候就无法完成分配了。这时候就该Mark-Compact出场了。
Mark-Compact
Mark-Compact是标记整理的意思,是在Mark-Sweep基础上演变而来。Mark-Compact在标记存活对象之后,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。
增量标记(Incremental Marking)
鉴于Node单线程的特性,V8每次垃圾回收的时候,都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复应用逻辑,被称为全停顿。
在分代垃圾回收中,一次小垃圾回收只收集新生代,且存活对象也相对较少,即使全停顿也没有多大的影响。但是在老生代中,存活对象较多,
垃圾回收的标记、清理、整理都需要长时间的停顿,这样会严重影响到系统的性能。所以增量标记被提出来。它从标记阶段入手,
将原本要一口气停顿完成的动作改为增量标记,拆分为许多小「步进」,每做完一「步进」就让JavaScript应用逻辑执行一小会,
垃圾回收与应用逻辑这样交替执行直到标记阶段完成。
内存泄露排查的工具
- node-heapdump
它允许对V8堆内存抓取快照,用于事后分析。 - node-profiler
它是alinode团队出品的类似node-heapdump的抓取内存堆快照的工具。教程: 如何使用Node Profiler