node内存控制之内存处理原理

一、V8垃圾回收机制与内存控制
1.Node与V8
Chrome成功的背后离不开javaScript引擎V8,V8的性能优势使得javaScript写高性能后台服务程序成为可能。在这样的契机下,Ryan Dahl 选择了JavaScript,选择了V8,

2.V8的内存限制
Node中通过JavaScript使用内存时只能使用部分内存,(64位系统下约1.4gb,32位系统下为0.7gb)。在这样的限制下,将会导致Node无法直接操作大内存对象,比如无法将一个2GB的文件读入内存中进行字符串分析处理,及时物理内存有32Gb,这样在单个Node进程的情况下,计算机的内存资源无法得到充分的使用。
3.V8的对象分配
Node提供了V8中内存使用量的查看方法
node
>process.memoryUsage()
{rss:1458592,
heapTotal:719504,
heapUsed:281496}
在上述代码中,在memoryUsage()方法的3个属性中,heapTotal和heapUsed是V8的堆内存使用情况,前者是已经申请的堆内存,后者是当前内存的使用的量:


在我们的代码中声明变量并付值时,所使用对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆得大小超过V8的限制为止,对于网页来说V8的限制已经绰绰有余了。深层原因是V8的垃圾回收机制。按官方的说法,以1.5GB的垃圾回收堆内存为例,V8做一次垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至需要1秒以上。这是垃圾回收中引起javaScript线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。
V8提供了选项让我们使用更多的内存,Node在启动时可以传递--max-old-space-size或--max-new-space-size来调整内存限制的大小示例如下:
node --max-old-space-size=1700 test.js //单位为MB
// 或者
node --max-new-space-size = 1024 test.js //单位为kb
上述设置在V8初始化时生效,一旦生效不能在动态改变。如果遇到Node无法分配足够内存给javaScript对象的情况,可以用这个方法放宽V8默认的内存限制
4.V8的垃圾回收机制
1.V8主要的垃圾回收算法
V8的垃圾回收策略主要基于分代式的垃圾回收机制,对象的生命周期长短不一,不同的算法只能针对特定情况具有最好的效果。为此,统计学在垃圾回收算法中产生了较大的作用,现代的垃圾回收算法中按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同分代的内存赋以不同的算法。
V8的内存分代
在V8中,主要内存分为新生代和老生代两代,新生代的对象为存活时间较短的对象,老生代中的对象为存活时间较长或者常驻内存的对象。


V8堆内存整体大小就是新生代所用内存空间加上老生代的内存空间。前面我们提及的--max-old-space-size 命令行参数可以用于设置老生代内存的最大值,--max-new-space-size设置新生代的值,启动后无法设置,所以V8使用的内存没有办法根据使用情况自动扩充,当内存分配中超过极限值时,会引起进程出错。
对于新生代内存,它由两个reserved_semispace_size_所构成,后面将描述其原因。按机器位数不同,reserved_semispace_size_在64位系统上分别为16M和8M。所以新生代内存的最大值在64位系统和32位系统上分别为32MB和16MB。
V8堆内存的最大保留空间可以从下面代码中看出来,其公式为4 * reserved_semispace_size_ + max_old_generation_size_
intptr_t MaxReserved() {
return 4* reserved_semispace_size_ + max_old_genration_size_
}
因此,默认情况下,V8堆内存的最大值64位系统上为1464MB,32位系统上则为732MB。
Scavenge算法
在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要采用Cheney算法,该算法由C.J.cheney与1970年首次发表在ACM论文上。
Cheney算法是一种采用复制的方式实现的垃圾回收算法,它将堆内存一分为二,每一部分空间称为semispace,在这两个semispace中,只有一个处于使用,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为TO空间,当分配对象时,先从From空间进行分配。当开始垃圾回收时,会检查From空间中存活对象,这些存活的对象将放在To空间中,而非存活对象占用的空间会被释放。完成复制后,From空间和To空间的角色发生兑换,简而言之,在垃圾回收的过程中,就是通过将存活对象的两个semispace空间之间进行复制
由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模的用到所有垃圾回收中,但是可以发现,Scavenge非常适合应用在新生代中,因为新生代中对象的生命周期较短。


当一个对象经过多次复制依然存活时,它将会认为是生命周期较长的对象。这种较长生命周期的对象随后会被转移到老生代中,采用新的算法进行管理。
在单纯的Scavenge过程中,From空间中的存活对象会复制到To空间中去,然后对From空间和To空间进行角色对换,但在分代式垃圾回收的前提下,from空间中的存活对象复制到To空间之前需要进行检查。在一定条件下,需要将存活周期长的对象移动到老生代中,也就是完成对象晋升。
对象晋升的条件主要有两个,一个是对象是否经历过Scavenge回收,一个是To空间的内存占用比超过限制。
在默认情况下,V8的对象分配主要集中在From空间中。对象从From空间中复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历了一次Scavenge回收,如果已经经历过,会将该对象从From空间复制到老生代空间中,如果没有,则复制到To空间中,

另一个判断条件是To空间的内存占用比。当要从From空间复制一个对象到To空间时,如果To空间已经使用了25%,则这个对象直接晋升到老生代空间中

设置25%这个限制的原因是当这次Scavenge回收完成后,这个To空间将会变成From空间,接下来的内存分配将在这个空间中进行,会影响后续的内存分配。
对象晋升后,将会在老生代空间中作为存活周期较长的对象来对待,接收新的回收算法处理。
Mark-sweep & mark-Compact
对于老生代的对象,由于存活对象占较大比重,在采用Scavenge的方式会有两个问题:一个是存活对象较多,复制存活对象的效率将会很低,另一个问题依然是浪费一半空间的问题。为此老生代采用Mark-sweep和mark-Compact。
Mark-sweep是标记清除的意思,Mark-Sweep在标记阶段遍历堆中所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。可以看出,Scavenge中只复制活着的对象,而Mark-Sweep只清理死亡对象。活着的对象在新生代中只占较小部分,死对象在老生代中只占小部分。

Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续内存分配造成问题,因为出现一个占大内存的文件,碎片化的空间无法完成分配,为了解决Mark-Sweep的内存碎片问题,Mark-Compact被提了出来,Mark-Compact是标记整理的意思,在Mark-Sweep的基础上演变而来的。他们的差别在于对象在标记为死亡后,在整理的过程中,将活着的对象往一段移动,移动完成后,直接清理掉边界的内存。

完成移动后,就可以直接清除最右边活着的对象后面的内存区域完成回收
V8在回收策略中两者是结合使用的。

在Mark-Sweep和Mark-Compact之间,由于Mark-Compact需要移动对象,所以它的执行速度不可能很快,V8中在空间不足以对从新生代中晋升过来的对象进行分配时才使用Mark-Compact
lncremental Marking
为了避免出现JavaScript应用逻辑与垃圾回收看到不一致的情况,垃圾回收的3种基本算法需要将应用逻辑暂停下来,在执行完垃圾回收再恢复执行应用逻辑,这种行为被称为“全停顿”(stop-this-world)。在V8的分代式垃圾回收中,一次小垃圾回收只收新生代,由于新生代配置较小,全停顿影响也不大。但V8的老生代通常配置得较大,且存活对象较多,全堆垃圾回收(full垃圾回收)的标记,清理,整理等动作造成的停顿就会比较可怕。需要设法改善。
为了降低全堆垃圾回收带来的停顿时间,V8先从标记入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小进步,每做完一步进,就让javaScript 应用逻辑执行一小会,垃圾回收与应用逻辑交替执行直到标记阶段完成。

V8在经历增量标记的改进后,垃圾回收的最大停顿时间可以减少原本的1/6左右, V8还引入了延迟清理(lazy sweeping)与增量式整理(incremental compaction)让清理和整理动作变成增量式。

原文地址:https://www.cnblogs.com/kbnet/p/13297855.html