该文章阅读需要7分钟,更多文章请点击本人博客halu886
V8的垃圾回收机制和内存限制
Nodejs类似Java,存在自动垃圾回收机制进行自动内存管理。当使用Javascript的场景从客户端切换到了服务器端时,内存管理的好坏,垃圾回收的是否优良,都与Nodejs的Javascript执行引擎v8息息相关。
Node 与 V8
Node 的发展离不开 V8 ,当初 Node 选型时,正值第三次浏览器大战,Chrome的V8引擎以卓越的性能遥遥领先。正因V8的卓越性能,node才能在服务端用Js实现高性能后端服务程序。
Node在执行上受益于V8,并且能够享受V8的迭代升级带来的更好的性能,以及对ES5/ES6语法特性,但是同时也受到V8的限制。
V8的内存限制
在V8,所有Javascript的对象内存都是基于堆的分配。并且Node提供了V8内存使用量的查看方式。
$ node
> process.memoryUsage();
{ rss:14754135,
heapTotal:7480983,
heapUsed: 4531543 }
heapTotal是堆总共申请的内存,heapUsed是正在被使用堆的内存。
当申明和定义变量时则会使用堆中的内存。如果已经存在空闲内存不够,则会申请新的堆内存,直到超过V8的内存限制。
至于为什么要限制内存的大小,表层原因是浏览器不需要这么大的内存。深层原因是V8的垃圾回收的限制,以1.5G内存为例,当V8进行小的垃圾回收时,耗时在50毫秒左右,做一次非增量式的垃圾回收甚至需要1s+,并且对于内存清理是会阻塞线程,在这样的事件开销下,应用的性能和响应速度都会直线下降,所以直接限制堆内存是一个好的选择。
不过,能在Node在启动时传递`–max-old-space-size或者–max-new-space-size来调整内存大小。
这个选项时在Node在启动时生效,一旦生效不能动态更改。当遇上内存不够,可以用以上方法放宽V8默认的内存限制,防止出现内存溢出,进程崩溃的情况。
V8的垃圾回收机制
在展开垃圾回收机制前,先简略介绍一下V8用到的各种垃圾回收算法。
V8主要的垃圾回收算法
V8的垃圾回收算法是基于分代式垃圾回收算法。没有任何一种算法能够解决所有的情况,因为在实际的应用中,对象的生存周期长短不一,不同的算法只能在特定情况下取得最好的效果。统计学对现代垃圾回收算法起到了较大的作用,并且根据对象的存活时间将垃圾回收进行不同的分代,然后分别对不同的分代内存采用更高效的效果,
V8的内存分代
在V8中,主要将内存分为新生代和老生代,新生代的对象存活时间较短,老生代为存活时间较长或者常驻内存的对象
V8堆的大小就是新生代的内存大小加上老生代的内存大小,并且可以通过--max-old-space
和--max-new-space
设置老生代和新生代的内存大小。不过必须要在进程初始化的时候指定,不能动态分配。当内存分配过程中超过极限值时这会引起进程出错。
在默认情况下,在64位的计算机和32位计算机的大小限制分别约为1.4G和0.7G,这个限制可以在源码中找到。Page::kPageSize的值为1MB。可以看到,老生代的设置为64位系统下为1400MB,在32位系统下为700MB
// semispce_size_ should be a power of 2 and old_generation_size_ should be
// a multiple of Page::kPageSize
#if defined(V8_TARGET_ARCH_X64)
#define LUMP_OF_MEMORY(2*MB)
code_range_size_(512*MB),
#else
#define LUMP_OF_MEMORY MB
code_range_size_(0),
#endif
#if defined(ANDROID)
reserved_semispace_size_(4*Max(LUMP_OF_MEMORY,Page::kPageSize)),
max_semispace_size_(4*Max(LUMP_OF_MEMORY,Page::kPageSize)),
initial_semispace_size_(Page::kPageSize),
max_old_generation_size_(192*MB),
max_executable_size_(max_old_generation_size_),
#else
reserved_semispace_size_(8*Max(LUMP_OF_MEMORY,Page::kPageSize)),
max_semispace_size_(8*Max(LUMP_OF_MEMORY,Page::kPageSize)),
initial_semispace_size_(Page::kPageSize),
max_old_generation_size_(700ul*LUMP_OF_MEMORY),
max_executable_size_(2561*LUMP_OF_MEMORY),
#endif
对于新生代的内存,是由两个reserved_semispace_size_决定的,reserved_semispace_size_在64位和32位机器上分别为16M和8M。所有新生代在64位和32位上的内存分别为32M和16M。
V8堆内存的最大保留空间可以从下面代码看出来,其公式为 4 * reserved_semispace_size_ + max_old_generation_size:
// Returns the maximum amount of memory reverved for the heap. For
// the young generation,we reserve 4 times the amount needed for a
// semi space. The young generation consists of two semi spaces and
// we reserve twice the amount needed for those in order to ensure
// that new space can be aligned to its size
intptr_t MaxReserved(){
return 4 * reserved_semispace_size_ + max_old_generation_size_;
}
因此默认情况下,V8堆内存在64位机器下为1464MB,在32位机器上为732MB。
Scavenge 算法
在分代的基础上,新生代是使用Scavenge算法进行回收,在Scavenge算法具体实现中,是使用cheney算法,是C.J.Cheney在1970首次发布在ACM上。
Cheney算法是一种是使用复制的垃圾回收算法,主要是使用将内存一分为二,都称为semispace,一个是在使用中,一个是空闲中。使用中的semispace称为from,空闲中的semispace称为to,每次进行垃圾回收时,将from中存在引用的复制到to中,然后清空from,然后两者再互换位置。
Scavenge算法的缺陷是只能使用堆内存的一半,但是这个由于划分空间和复制的导致,但是由于新生代中,每次垃圾回收时存活的对象比较少,所以在性能上有有优异的表现。
Scavenge算法是典型的空间换时间,所以无法大范围的使用,但是因为新生代的生命周期比较短,所以恰恰适合这个算法。
V8堆内存示意图
堆内存的大小实际上是两个semispace和老生代所用内存之和。
当一个对象经过多次复制仍然存活,则会被移动到老生代中,这个过程被称为晋升。
在单纯的Scavenge中,会将from中的存活对象复制到to中,然后对两个semispace进行翻转,但是在分代式下,每次复制前都会进行检查,如果存活周期较长,则进行对象晋升。
对象晋升的主要条件是两个,是否经历过Scavenge,或者To空间的内存占用比超过限制。
在默认情况下,V8分配对象空间是在from空间,然后再scavenge时,判断这个对象是否经历过Scavenge回收。如果是,则将对象晋升为老生代,否则就复制到to空间。
另一个是内存占用比,当进行复制时,如果to空间内存占用比超过25%,则直接晋升到老生代中。
使用25%进行限制,是因为复制完后,to则会翻转为from,如果占用比过高,则会影响后面的内存分配。
当对象在老生代中,则会接收新的回收算法处理。
Mark-Sweep & Mark-Compact
对于老生代的对象来说,存活对象比较多,如果再使用scavenge则会有两个问题,复制存活对象效率太低,同时浪费一般的内存空间。所以V8采用Mark-Sweep与Mark-Compact相结合的方式。
Mark-Sweep会将没被引用的对象清除,由于老生代中,未存活的对象只占很小的一部分,Mark-Sweep更合适,以下是示意图:
Mark-Sweep会造成一个问题,当进行清理完后,所造成的内存空间是不连续的,如果此时需要分配一个比较大对象,碎片空间是无法完成此次分配的,则会提前触发垃圾回收,但是这是不必要的。
为了解决Mark-Sweep的问题,则Mark-Compact在这个基础上被提出,在标记的过程中,将存活的对象向一端移动,移动完毕后,统一将死亡对象回收。如图所示,白色格子为存货对象,浅色为空间,深色为死亡对象。
下图是V8算法的简单对比。
从表中可以看出,Mark-Compact需要移动对象,所以比较耗时。所以取舍上,V8主要使用Mark-Sweep。只有当内存不能分配新生代对象对象时,才执行Mark-Compact。
Incremental Marking
为了避免Javascript应用逻辑运算和垃圾回收器结果不一致,三种算法执行时会在垃圾回收时将Javascript应用逻辑暂停,这个逻辑称为全停顿(stop-the-world)。每次小的垃圾回收,新生代配置较少,影响不大,然而老生代分配的比较多,一旦阻塞对业务影响挺大的,需要优化。
为了降低全堆垃圾回收的卡顿,将一口气的标记动作改为了增量标记(incremental marking),也就是每标记一小部分,执行一下Javascript应用逻辑,然后再继续标记,直至标记阶段完成。
V8经过增量标记优化后,最大延迟时间减小到原来的1/6了。
V8后续还引入了延迟清除(lazy sweeping)和增量整理(incremental compaction),让清理和整理动作也变为增量式的了。
以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)
你说的还行,这个事情我比较认可 现在我开始说了 ** 哈哈哈**
- 现在我们
- 你是谁