node Buffer

在说Buffer这个模块之前,先来讨论一下node内存的问题。
我们在代码中声明变量并且赋值时,所使用的对象的内存分配在堆中。如果已申请的对空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过v8限制为止。
为什么v8要限制堆的大小,表层原因因为v8最初为浏览器而设计,不太可能遇到用大量内存的场景。对于网页来说,v8的限制已经绰绰有余。深层原因是垃圾回收机制的限制。

在浏览器中,JavaScript直接处理字符串即可满足 大多数的业务需求,而node则需要处理网络流和文件I/O流,还要处理大量二进制数据,操作字符远远不能满足传输性能的需求,于是Buffer就应运而生。Buffer不同于其他对象,它不经过v8内存分配机制,所以也不会有内存大小的限制。

node内存构成主要由通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存。

大概了解node内存的问题之后,让我们来看看node buffer具体问题:

Buffer结构

Buffer是一个像Array的对象,但它主要用于操作字节。

Buffer模块结构

Buffer是一个典型的JavaScript与C++结合的模块,它将性能相关部分用C++实现,将性能的相关部分用JavaScript实现。Node在进程启动时就已经加载了它,并将其放在全局对象(global)上。所以在使用Buffer时,无须通过require()即可直接使用。

Buffer对象

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编码下占用三个元素,字母和半角标点符号占用一个元素。Buffer受Array类型的影响也很大,可以访问length属性,也可以通过下标访问元素。

Buffer内存分配

Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面上,实现内存的申请,因为处理大量的字节数据,不能采用一点点的内存就向操作系统申请一点内存的方式,这可能造成大量的内存申请的系统调用,对操作系统有一定的压力。为此node在内存的使用上应用的是C++层面上的申请内存,在JavaScript中分配内存的策略。
为了高效的使用申请来的内存,Node 采用slab分配机制。slab是一种动态内存管理机制。
简单的来说slab就是一块申请好的固定大小的内存区域。slab具有如下三种状态:
full:完全分配状态
partial:部分分配状态
empty:没有被分配状态

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

new Buffer(size)

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

Buffer.poolSize = 8*1024;

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

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

var pool;
function allocPool(){
  pool = new SlowBuffer(Buffer.poolSize);
  pool.used = 0;
}

构造小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;

这个时候slab的状态是partial。
当再次创建一个Buffer对象的时候,构造过程会判断这个slab的剩余空间是否足够。如果足够,使用剩余空间,并更新slab的分配状态。如果slab剩余的空间不够,将会重新构造新的slab,原slab中剩余的空间会造成浪费。
例如:第一次构造一字节的Buffer对象,第二次构造8192字节的Buffer对象,由于第二次分配时slab中的额空间不够,所以创建并使用新的slab,第一个slab的8KB将会被第一个1字节的Buffer对象独占。下面的代码一共使用了两个slab单元:

new Buffer(1);
new Buffer(8192);

我们需要注意的是:由于同一个slab可能分配给多个Buffer对象使用,只有这些小Buffer对象在作用域释放并都可以回收的时候,slab的8KB空间才会被回收。尽管创建了1字节的Buffer对象,但是如果不释放它,实际可能是8KB的内存没有释放。

分配大的Buffer对象

如果需要超过8KB的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的堆中。

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

字符串转Buffer

字符串转Buffer对象主要是通过构造函数完成的:

new Buffer(str,[encoding]);

通过构造函数转换的Buffer对象,存储的只能是一种编码类型。encoding参数不传递时,默认按照UTF-8进行转码和存储。

一个Buffer对象可以存储不同编码类型的字符串转码的值,调用write()方法可以实现目的:

buf.writestring,[offset],[length],[encoding]);

由于可以不断写入内容到Buffer对象中,并且每次写入可以指定编码,所以Buffer对象中可以存在多种编码转化后的内容。需要注意的是,每种编码所用的字节长度不同,将Buffer反转回字符串需要谨慎处理。

Buffer转字符串

实现Buffer转字符串的转换也很简单,Buffer对象的toSteing()可以将Buffer()对象转换为文字符串,代码如下:

buf.toString();
Buffer与性能

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

通过预先转换静态内容为Buffer对象,可以有效地减少CPU的重复使用,节省服务器资源。在Node构建web应用中,可以选择将页面的动态内容和静态内容分离,静态内容部分可以通过预先转换为Buffer的方式,使性能得到提升。由于文件是二进制数据,所以在不需要改变内容的场景下,尽量只读取Buffer,然后直接传输,不做额外的转换,避免损耗。

文件读取

Buffer的使用除了与字符串的转换有性能损耗外,在文件读取时,又一个highWaterMark设置对性能的影响至关重要。在fs.createReadStream(path,opts)时,我们可以传入一些参数,代码如下:

{
flags:'r',
encoding:null,
fd:null,
mode:0666,
highWaterMark:64*1024
}

我们还可以传递start和end来指定读取文件的位置范围:

{start:90,end:99}

fs.createReadStream()的工作方式是在内存中准备一段Buffer,然后在fs.read()读取时逐步从磁盘将字节复制到buffer。完成一次读取时,则从这个Buffer中通过slice()方法读取出部分数据作为一个小的Buffer对象,再通过data事件传递给调用发。如果Buffer用完,则重新分配一个,如果还有剩余,则继续使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值