您的位置:首页 > Web前端 > Node.js

node的内存控制

2017-03-26 15:29 162 查看
*本文内容部分来自《深入浅出node.js》

js作为一门脚本语言能在服务端大放异彩最重要的原因就是高性能的v8引擎,但是v8引擎也有一些限制,例如内存方面的限制,本文主要讲解v8引擎的内存控制。

node通过js来使用内存时只能使用部分内存(64位系统下约为1.4GB,32位系统下约为0.7GB),这将会导致node无法操作大内存对象。造成这个问题的主要原因在于node基于v8构建,而v8限制了内存的用量。

在v8中,所有的js对象都是通过堆来分配的,当我们在代码中声明变量并赋值时,所使用的对象内存分配在堆中,如果已申请的堆空间内存不够分配新的对象时,将继续申请堆内存,直到堆的大小超过v8的限制为止。至于v8为何要限制堆的大小,原因是v8的垃圾回收机制。1.5GB的垃圾回收堆内存,v8做一次小的垃圾回收需要50ms以上,做一次非增量的垃圾回收则需要1s以上,而垃圾回收会引起js执行线程暂时停止执行,这种时候,应用的性能和响应能力会直线下降,因此,v8在设计的时候直接限制了堆内存的大小。接下来,我们深入了解一下v8的垃圾回收机制。

v8的垃圾回收机制

v8的垃圾回收策略主要基于分代式回收机制,因为在内存中,不同的对象存在的时间与生成的频率有很大的差别,所以在v8中,主要将堆内存分为新生代和老生代两代。新生代中的对象为存活时间较短的对象,老生代对象为存活时间较长的对象或常驻对象。使用--max-old-space-size命令可以设置老生代内存的最大值,使用--max-new-space-size可以设置新生代内存的最大值,这两个方法可以放宽v8的默认内存,但是这两个方法只能在node进程启动时指定,这意味着v8的使用内存无法根据使用情况自动扩充。新生代和老生代的内存限制可以在v8的源码中找到。新生代内存由两个reserved_semispace_size_所构成,而reserved_semispace_size_在64位系统和32位系统上分别为16MB和8MB,所以新生代内存在64位系统和32位系统中分别为32MB和16MB。v8堆内存最大保留空间为4*reserved_semispace_size_
+ max_old_generation_size_,因此,默认情况下,v8堆内存最大值在64位系统上为1464MB,在32位系统上为732MB。

在v8的分代机制中,新生代和老生代使用的回收算法也不一致。新生代中对象主要通过Scavenge算法来进行垃圾回收,该算法采用复制的方式实现垃圾回收算法,它将堆内存一分为二,每一部分空间称为semispace,在这两个semispace空间中,只有一个处于使用中,称为From空间,另一个处于空闲状态,称为To空间。当我们分配对象时,在From空间中进行分配,而开始进行垃圾回收时,会检查From空间中的存活对象(具体表现为被引用对象),这些存活对象将被剪切到To空间,而非存活对象占用的空间将会被释放。完成这个过程后,From空间和To空间将会对调。简而言之,就是通过将存活对象在两个semispace空间之间进行剪切。这也是为什么新生代堆空间大小是2*reserved_semispace_size_。Scavenge的缺点是只能使用一半的堆空间,但是Scavenge只复制存活的对象,对于生命周期短的场景,存活对象只有很少一部分,所以Scavenge在时间效率上有有优异的表现,是典型的空间换取时间的算法。而新生代中对象的生命周期较短,比较适合这个算法。

新生代对象在经过多次剪切之后依然存活时,它将会被认为是生命周期较长的对象,这中对象随后会被移动到老生代中,采用新的算法管理。这种移动称为晋升。对象晋升的条件主要有两个,一个是对象是否经历过Scavenge回收,一个是To空间的内存占用超过25%。设置25%这个限制是因为当这次Scavenge回收完成之后,To空间将会变成From空间,接下来的内存分配将会在这个空间中完成,如果占用比过高,将会影响后续内存分配。

对于老生代的对象,主要使用Mark-Sweep&Mark-Compact算法进行垃圾回收。Mark-Sweep是标记清除的意思,在垃圾回收时,Mark-Sweep在标记阶段遍历堆中所有的对象,并标记活着的对象,在随后的清除阶段,清除掉所有未标记的对象。而在老生代中,大部分是生存周期较长的对象或者常驻对象,所以采用这种方式比较高效。



但是Mark-Sweep存在一个问题,就是在进行一次标记清除之后,内存空间会出现不连续的状态,这种内存碎片会对后续内存分配造成问题,因此Mark-Compact算法被提出来。Mark-Compact是标记整理的意思,在整理过程中,将活着的对象往一边移动,移动完成之后,清理掉边界外的内存。如上图所示,将会留出大块的内存空间。

因为相对前者后者的回收速度是比较慢的,因为它有对象的移动,而mark-sweep没有对象移动,所以,效率会比较高。V8在清理时主要会使用Mark-sweep,在空间不足以对新生代中晋升过来的对象进行分配时才会使用Mark-compact。

以上提到的几种垃圾回收算法都需要将应用逻辑停下来,等完成垃圾回收后再恢复继续执行,即“stop-the-world”,在这点上V8也做了优化。一次小的垃圾回收只收集新生代,而新生代存活对象少,全停顿影响不大。而对于老生代,则进行增量标记,拆分成许多小“步进”,每做完一“步进”,就让应用逻辑执行一会,同时运用延迟清理和增量式整理来优化,具体不再展开。 

查看垃圾回收日志

在启动时添加--trace_gc参数,可以在垃圾回收时从标准输出中打印垃圾回收的日志信息。





我截取了其中一部分的截图,可以看到Scavenge回收占据了大部分。

内存指标



node提供了process.memoryUsage()方法查看内存使用情况



rss是进程的常驻内存部分,heaptotal是堆中总共申请的内存量,heapused是堆中目前使用的内存量。从结果来看,堆中内存占用量总是小于进程的常驻内存用量,这是因为node的内存使用并非都是通过V8进行分配的,这种不是通过V8分配的内存叫堆外内存,堆外内存主要是Buffer对象引起,在本文中暂时不解释Buffer对象。但是这意味着堆外内存可以用来突破内存限制的问题。

内存泄漏及解决方案

node运行在服务端,因此对内存泄露十分敏感,通常造成内存泄漏的原因有这么几个:

1.缓存

缓存在应用中的作用很重要,可以十分有效的节省资源。但是一个对象一旦被当做缓存使用,它将常驻在老生代中,缓存中存储的键越多,长期存活的对象也越多,这将导致在垃圾回收机进行扫描和整理的时候做很多无用功。因此我们需要对缓存做限制,一种可行性方案是将缓存键记录在数组中,一旦超过数量,就以先进先出的方式进行淘汰。对于大量缓存,最好是使用进程外的缓存比如Redis之类的来解决。

2.队列

队列的堆积会导致相关作用域得不到释放,内存占用不会回落,造成内存泄露。对于这种场景,我们之前介绍过的Bagpipe提供了超时模式和拒绝模式,可以有效的解决该问题。

3.作用域与闭包

var foo =function(){
var local='local var ';
var bar =function(){
var local='another var';
var baz = function(){
console.log(local);
};
baz();
};
bar();
};
foo();

在上面的例子中,baz在打印local变量时,会在baz范围内找,如果没有,就会往上一级,找到bar里面的local,打印出another var,如果删除another var ,则会打印local var的值。这就是作用域链,如果在本级找不到就会往上级寻找,直到全局作用域。所以经常我们的代码里面有一些引用如果不清除就可能会导致内存泄漏,对于这种情况,建议在操作完成之后进行释放。可以使用delete操作符或者赋值为空来解决,在v8里面使用delete操作符会影响v8的优化,所以通过赋值为空的方式来解决是最好的。

对于闭包,实现外部作用域访问内部作用域中的变量的方式就叫闭包,通常是通过一个高阶函数来实现的,也就是把函数作为参数传递,对于这种情况,我们可以在操作完成之后手动释放内部作用域。

9504
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  javascript node 内存