您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
Node.js的内存管理 .

 
来源:csdn 发布于2017-8-28
   次浏览      
 

一、V8的垃圾回收机制与内存限制

Node使用chrome的V8作为JS脚本引擎,因此Node的内存管理与V8关系很密切。

1.V8的内存限制

因为V8是为浏览器环境设计的,所以内存限制很小,在64位系统下约为1.4 GB,32位系统下约为0.7 GB。

2.V8的对象分配

在V8中,所有的JavaScript对象都是通过堆来进行分配的。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过V8的限制为止。

至于V8为何要限制堆的大小,表层原因为V8最初为浏览器而设计,不太可能遇到用大量内存的场景。对于网页来说,V8的限制值已经绰绰有余。深层原因是V8的垃圾回收机制的限制。按官方的说法,以1.5 GB的垃圾回收堆内存为例,V8做一次小的垃圾回收需要50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起JavaScript线程暂停执行的时间,在这样的时间花销下,应用的性能和响应能力都会直线下降。这样的情况不仅仅后端服务无法接受,前端浏览器也无法接受。因此,在当时的考虑下直接限制堆内存是一个好的选择。”

3.垃圾回收机制

V8使用分代式回收: 把内存分为新生代和老生代。 对新生代使用复制算法(内存一分二,分为From和To),对老生代使用标记清除和标记整理算法。

在默认情况下,V8的对象分配主要集中在From空间中。对象从From空间中复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收。如果已经经历过了,会将该对象从From空间复制到老生代空间中,如果没有,则复制到To空间中。另一个判断条件是To空间的内存占用比。当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代空间中。之后From和To地位互换。

在Mark-Sweep和Mark-Compact之间,由于Mark-Compact需要移动对象,所以它的执行速度不可能很快,所以在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时才使用Mark-Compact。

增量标记

为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿”(stop-the-world)。

在V8的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置得较小,且其中存活对象通常较少,所以即便它是全停顿的影响也不大。但V8的老生代通常配置得较大,且存活对象较多,全堆垃圾回收(full 垃圾回收)的标记、清理、整理等动作造成的停顿就会比较可怕,需要设法改善。

为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一

“步进”就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。

V8后续还引入了延迟清理(lazy sweeping)与增量式整理(incremental compaction),让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行清理,进一步利用多核性能降低每次停顿的时间。

4.堆外内存

Node中的内存使用并非都是通过V8进行分配的。我们将那些不是通过V8分配的内存称为堆外内存。

Buffer对象不同于其他对象,它不经过V8的内存分配机制,所以也不会有堆内存的大小限制。

这意味着利用堆外内存可以突破内存限制的问题。

为何Buffer对象并非通过V8分配?这在于Node并不同于浏览器的应用场景。在浏览器中,JavaScript直接处理字符串即可满足绝大多数的业务需求,而Node则需要处理网络流和文件I/O流,操作字符串远远不能满足传输的性能需求。

二、内存泄露

通常原因:

缓存

队列消费不及时

作用域未释放

1.慎用内存做缓存

比如underscore的memorize方法。

可以利用进程外缓存, 比如Redis或者Memcached

2.队列的问题

要监控队列长度,一旦堆积,要通过监控系统发出警报

另一个方案: 给异步调用一个超时机制,从而给队列消费速度一个下限。

3.就是闭包的问题。

三、大内存应用

在Node中,不可避免地还是会存在操作大文件的场景。由于Node的内存限制,操作大文件也需要小心,好在Node提供了stream模块用于处理大文件。

stream模块是Node的原生模块,直接引用即可。stream继承自EventEmitter,具备基本的自定义事件功能,同时抽象出标准的事件和方法。它分可读和可写两种。Node中的大多数模块都有stream的应用,比如fs的createReadStream()和createWriteStream()方法可以分别用于创建文件的可读流和可写流,process模块中的stdin和stdout则分别是可读流和可写流的示例。

由于V8的内存限制,我们无法通过fs.readFile()和fs.writeFile()直接进行大文件的操作,而改用fs.createReadStream()和fs.createWriteStream()方法通过流的方式实现对大文件的操作。

四、理解Buffer

1.基本用法

由于Buffer太过常见,Node在进程启动时就已经加载了它,并将其放在全局对象(global)上。所以在使用Buffer时,无须通过require()即可直接使用。

Buffer对象类似于数组,它的元素为16进制的两位数,即0到255的数值。示例代码如下所示:

var str = "深入浅出node.js";
var buf = new Buffer(str, 'utf-8');
console.log(buf);
// => <Buffer e6 b7 b1 e5 85 a5 e6 b5 85 e5 87 ba 6e 6f 64 65 2e 6a 73>

由上面的示例可见,不同编码的字符串占用的元素个数各不相同,上面代码中的中文字在UTF-8编码下占用3个元素,字母和半角标点符号占用1个元素。

Buffer受Array类型的影响很大,可以访问length属性得到长度,也可以通过下标访问元素,在构造对象时也十分相似,代码如下:


var buf = new Buffer(100);
console.log(buf.length); // => 100

上述代码分配了一个长100字节的Buffer对象。可以通过下标访问刚初始化的Buffer的元素,代码如下:

console.log(buf[10]);

这里会得到一个比较奇怪的结果,它的元素值是一个0到255的随机值。

值得注意的是,如果给元素赋值不是0到255的整数而是

小数时会怎样呢?示例代码如下所示:

buf[20] = -100;
console.log(buf[20]); // 156
buf[21] = 300;
console.log(buf[21]); // 44
buf[22] = 3.1415;
console.log(buf[22]); // 3

给元素的赋值如果小于0,就将该值逐次加256,直到得到一个0到255之间的整数。如果得到的数值大于255,就逐次减256,直到得到0~255区间内的数值。如果是小数,舍弃小数部分,只保留整数部分。

2.内存分配

Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存的申请的。因为处理大量的字节数据不能采用需要一点内存就向操作系统申请一点内存的方式,这可能造成大量的内存申请的系统调用,对操作系统有一定压力。为此Node在内存的使用上应用的是在C++层面申请内存、在JavaScript中分配内存的策略。

为了高效地使用申请来的内存,Node采用了slab分配机制。slab是一种动态内存管理机制,最早诞生于SunOS操作系统(Solaris)中,目前在一些*nix操作系统中有广泛的应用,如FreeBSD和Linux。

简单而言,slab就是一块申请好的固定大小的内存区域。slab具有如下3种状态。

full:完全分配状态。

partial:部分分配状态。

empty:没有被分配状态。

当我们需要一个Buffer对象,可以通过以下方式分配指定大小的Buffer对象:

new Buffer(size);

Node以8 KB为界限来区分Buffer是大对象还是小对象:

Buffer.poolSize = 8 * 1024;

这个8 KB的值也就是每个slab的大小值,在JavaScript层面,以它作为单位单元进行内存的分配。

1.分配小对象

如果指定Buffer的大小少于8 KB,Node会按照小对象的方式进行分配。Buffer的分配过程中主要使用一个局部变量pool作为中间处理对象,处于分配状态的slab单元都指向它。以下是分配一个全新的slab单元的操作,它会将新申请的SlowBuffer对象指向它:

在图6-2中, slab处于empty状态。

构造小Buffer对象时的代码如下:

new Buffer(1024);

这次构造将会去检查pool对象,如果pool没有被创建,将会创建一个新的slab单元指向它:

if (!pool || pool.length - pool.used < this.length) allocPool();

同时当前Buffer对象的parent属性指向该slab,并记录下是从这个slab的哪个位置(offset)开始使用的,slab对象自身也记录被使用了多少字节,代码如下:

this.parent = pool;
this.offset = pool.used;
pool.used += this.length;
if (pool.used & 7) pool.used = (pool.used + 8) & ~7;

当再次创建一个Buffer对象时,构造过程中将会判断这个slab的剩余空间是否足够。如果足够,使用剩余空间,并更新slab的分配状态。

2.分配大Buffer对象

如果需要超过8 KB的Buffer对象,将会直接分配一个SlowBuffer对象作为slab单元,这个slab单元将会被这个大Buffer对象独占。

// Big buffer, just alloc one
this.parent = new SlowBuffer(this.length);
this.offset = 0;

这里的SlowBuffer类是在C++中定义的,虽然引用buffer模块可以访问到它,但是不推荐直接操作它,而是用Buffer替代。

上面提到的Buffer对象都是JavaScript层面的,能够被V8的垃圾回收标记回收。但是其内部的parent属性指向的SlowBuffer对象却来自于Node自身C++中的定义,是C++层面上的Buffer对象,所用内存不在V8的堆中。

3.Buffer的转换和拼接

字符串和Buffer之间可以互相转换,只要定义编码类型即可。

正确的拼接方式是用一个数组来存储接收到的所有Buffer片段并记录下所有片段的总长度,然后调用Buffer.concat()方法生成一个合并的Buffer对象。

4.Buffer与性能

Buffer在文件I/O和网络I/O中运用广泛,尤其在网络传输中,它的性能举足轻重。在应用中,我们通常会操作字符串,但一旦在网络中传输,都需要转换为Buffer,以进行二进制数据传输。在Web应用中,字符串转换到Buffer是时时刻刻发生的,提高字符串到Buffer的转换效率,可以很大程度地提高网络吞吐率。

   
次浏览       
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程