Buffer
- v8内存分配 —— 堆内存
- 不是通过V8进行分配的内存 —— 堆外内存(如Buffer)
定义
Buffer是一个像Array的对象,但它主要作用于操作字节。
内存分配
Buffer对象的内存分配不是在v8的堆内存中,而是在node的C++层面实现的。
因为要处理大量数据,不能像堆内存一样,需要一点申请一点,所以使用的是在C++层面申请,JS中分配策略。
为了高效使用申请来的内存,Node采用slab分配机制。简而言之slab就是一块申请好的固定大小的内存区域。slab具有如下三种状态:
- full:完全分配状态
- partial:部分分配状态
- empty: 没有被分配状态
当我们需要Buffer对象,可以通过以下方式指定Buffer大小:
new Buffer(size);
Node是以8 KB为界限来区分Buffer是大对象还是小对象,这个8kb也是每个slab的值,在js层面,以它作为单位单元进行内存分配。
分配小Buffer对象
如果制定的size<8kb,Node会按照小对象分方式进行分配。分配过程中主要是用一个局部变量pool作为中间对象,处于分配状态的slab单元都指向它。
var pool;
function allocPool() {
pool = new SlowBuffer(Buffer.poolSize);
pool.used = 0;
}
此时slab处于empty状态。
构造小Buffer对象的代码如下:
new Buffer(1024);
这次构造将会去检查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的分配状态。下面是创建一个新的Buffer对象,将引起slab的再次分配:
new Buffer(3000);
如果slab空间不足,将会构造新的slab。原slab中剩余空间将造成浪费。
要注意的是,由于同以slab分配给不同的Buffer使用,只有这些小Buffer对象在作用域释放并都回收时,slab的8KB空间才会被回收。
分配大Buffer对象
如果需要超过8KB的Buffer对象,将会直接分配一个SlowBuffer对象作为slab单元,这个slab单元将会被这个Buffer独占。
// Big buffer, just alloc one
this.parent = new SlowBuffer(this.length);
this.offset = 0;
上面提到的Buffer对象都是js层面的,能够被V8标记回收。但其内部的parent属性指向的SlowBuffer对象却来自于Node自身C++中的定义,是C++层面上的Buffer对象。
小结
真正的内存是在Node的C++层面提供的,js层面只是使用它。当进行小而频繁的Buffer操作时,采用slab的机制进行预先申请和事后分配,使得js到操作系统之间不用有太多的内存申请方面的调用。而对于大块的Buffer而言,则直接使用C++层面提供的内存,无需细腻的分配操作。
Buffer的转换
字符串转Buffer
构造函数转换Buffer对象,存储的只能是一种编码类型。encoding不传递是默认按UTF-8编码进行转码和存储。
new Buffer(str, [encoding]);
一个Buffer对象可以存储多种不同类型的字符串转码的值,但美中编码的长度不同,从Buffer转回字符串时要谨慎处理:
buf.write(string, [offset], [length], [encoding])
Buffer转字符串
buf.toString([encoding], [start], [end])
Buffer不支持的编码类型
提供一个isEncoding()方法来判断编码是否支持转换,支持返回true,否则为false:
Buffer.isEncoding(encoding)
Buffer的拼接
var fs = require('fs');
var rs = fs.createReadStream('test.md');
var data = '';
rs.on("data", function (chunk){ 7
data += chunk; });
rs.on("end", function () { console.log(data);
});
以上代码中,data事件中获取的chunk就是Buffer对象 ,这句data拼接中隐藏了toString操作,它等价于:
data = data.toString() + chunk.toString();
注意的是上述方法,在读取宽字节的中文时会产生乱码问题。我们模拟场景,将每次读取的Buffer长度设置为11:
var rs = fs.createReadStream('test.md', {highWaterMark: 11});
// 测试内容为《静夜思》
// 输出 -----> 窗前���光,疑���地上霜;举头���明月,���头思故乡
乱码产生原因
buf.toString()方法默认以UTF-8为编码,中文在UTF-8下占3个字节,而我们每次的读取长度只有11,所以第一个Buffer对象在输出时只能显示3个字符,Buffer中剩下的两个字节不足以显示文字,所以显示为2个�乱码。第二次输出中的第一个字节也不足以表示问题,也显示为1个�乱码。
正确拼接Buffer
正确的方法是,用一个数组来存储接收到的所有Buffer片段,并记录下所有片段的总长度,然后调用Buffer.concat()生成一个合并的Buffer对象。
var chunks = [];
var size = 0;
res.on('data', function (chunk) {
chunks.push(chunk);
size += chunk.length;
});
res.on('end', function () {
var buf = Buffer.concat(chunks, size); var str = iconv.decode(buf, 'utf8');
console.log(str);
});
Buffer与性能
Buffer在文件I/O与网络I/O中运用十分广泛。在应用中我们通常会操作字符串,但一旦在网络中传输,都要转换成Buffer,以进行二进制传输。
通过预先转换静态内容为Buffer对象,可以有效的减少CPU的重复使用,节省服务器资源。