Buffer 是一个 Javascript 与 C++ 结合的模块,它将性能相关部分用 C++ 实现,将非性能相关的部分用 Javascript 实现。
Buffer 所占用的内存不是通过 V8 分配的,属于堆外内存。由于 Buffer 太过常见,Node 进程在启动时就已经加载了它,并将其放在全局对象(global)上。所以在使用 Buffer 时,无须通过 require() 即可直接使用。
Buffer 对象
Buffer 对象类似于数组,它的元素为 16 进制的两位数,即 0 到 255。
let str = '学习node.js';
let buf = new Buffer(str, 'utf-8');
console.log(buf);
// <Buffer e5 ad a6 e4 b9 a0 6e 6f 64 65 2e 6a 73>
由上面的示例可见,不同编码的字符串占用的元素个数各不相同,上面代码的中文在 UTF-8 编码下占用 3 个元素,字母哈半角标点符号占用 1 个元素。
Buffer 受 Array 类型的影响很大,可以访问 length 属性得到长度,可以通过下标访问元素,在构造对象时也十分相似:
let buf = new Buffer(100);
console.log(buf.length); // 100
如果给元素赋值不是 0 到 255 的整数或者小数会怎样呢?
buf[20] = -100;
console.log(buf[20]); // 156
buf[20] = 300;
console.log(buf[20]); // 44
buf[20] = 3.14;
console.log(buf[20]); // 3
给元素的赋值如果小于 0,就将该值逐次加 256,直到得到一个 0 到 255 之间的整数。如果得到的数值大于 255,就逐次减 256,直到得到 0 到 255 区间内的数值。如果是小数,舍弃小数部分,只保留整数部分。
Buffer 内存分配
Buffer 对象的内存分配不在 V8 的堆内存中,而是在 Node 的 C++ 层面实现内存的申请。因为处理大量的字节数据不能采用需要一点内存就向操作系统申请一点内存的方式,这可能造成大量的内存申请的系统调用。因此 Node 在内存的使用上应用的是在 C++ 层面申请内存、在 Javascript 中分配内存的策略。
为了高效使用申请来的内存,Node 采用了 slab 分配机制。简单而言,slab 就是一块申请好的固定大小的内存。slab 具有如下 3 种状态:
- full:完全分配状态
- partial:部分分配状态
- empty:未被分配状态
Node 以 8KB 为界限来区分 Buffer 是大对象还是小对象:
Buffer.poolSize = 8 * 1024;
这个 8KB 的值也就是每个 slab 的大小,在 Javascript 层面,以此为单元进行内存的分配。
分配小 Buffer 对象
如果指定 Buffer 的大小小于 8KB,Node 会按照小对象的方式进行分配。Buffer 分配过程中主要使用一个局部变量 pool 作为中间处理对象,处于分配状态的 slab 单元都指向它。以下是分配一个全新的 slab 单元的操作,它会指向新申请的 SlowBuffer 对象:
let pool;
function allocPool() {
pool = new SlowBuffer(Buffer.poolSize);
pool.used = 0;
}
此时,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;
这时 slab 状态为 partial。
当再次创建一个 Buffer 对象时,构造过程中将会判断这个 slab 的剩余空间是否足够。如果足够,使用剩余空间,并更新 slab 的分配状态。
如果 slab 的剩余空间不够,将会构造新的 slab,原 slab 中剩余的空间会造成浪费。例如,第一次构造 1 字节的 Buffer 对象,第二次构造 8192 字节的 Buffer 对象,由于第二次分配 slab 中的空间不够,所以会创建并使用新的 slab,第一个 slab 的 8KB 将会被第一个 1 字节的 Buffer 对象独占。
这里要注意的是,由于同一个 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;
上面提到的 Buffer 对象都是 Javascript 层面的,能被 V8 的垃圾回收标记回收。但是其内部的 parent 指向的 SlowBuffer 对象却来自于 Node 自身 C++ 中的定义,是 C++ 层面上的 Buffer 对象,所用内存不在 V8 的堆中。
小结
真正的内存实在 Node 的 C++ 层面提供的,Javascript 层面只是使用它。当进行小而频繁的 Buffer 操作时,采用 slab 的机制进行预先申请和事后分配,使得 Javascript 到操作系统之间不必有过多的内存申请方面的系统调用。对于大块的 Buffer 而言,则直接使用 C++ 层面提供的内存,无需细腻的分配操作。