探究不在V8堆内存中存储的Buffer对象

前言

写完上一篇文章想学Node.js,stream先有必要搞清楚
留下了悬念,stream对象数据流转的具体内容是什么?本篇文章将为你们进行深刻讲解。javascript

Buffer探究

看一段以前使用stream操做文件的例子:java

var fileName = path.resolve(__dirname, 'data.txt');
var stream=fs.createReadStream(fileName);
console.log('stream内容',stream);  
stream.on('data',function(chunk){
    console.log(chunk instanceof Buffer)
    console.log(chunk);
})

看一下打印结果,发现第一个stream是一个对象 ,截图部份内容。node


第二个和第三个打印结果,c++


Buffer对象,相似数组,它的元素为16进制的两位数,即0到255的数值。能够看出stream中流动的数据是Buffer类型,二进制数据,接下来开始咱们的Buffer探索之旅。git

什么是二进制

二进制是计算机最底层的数据格式,字符串,数字,视频,音频,程序,网络包等,在最底层都是用二进制来进行存储。这些高级格式和二进制之间,均可以经过固定的编码格式进行相互转换。程序员

例如,C语言中int32类型的十进制整数(无符号),就占用32bit即4byte,十进制的3对应的二进制就是00000000 00000000 00000000 00000011。字符串也是同理,能够根据ASCII编码规则或者unicode编码规则(如utf-8)等和二进制进行相互转换。总之,计算机底层存储的数据都是二进制格式,各类高级类型都有对应的编码规则和二进制进行相互转换。github

node中为何会出现Buffer这个模块

在最初的javascript生态中,javascript还运行在浏览器端,对于处理Unicode编码的字符串数据很容易,可是对于处理二进制以及非Unicode编码的数据无能为力,可是对于Server端操做TCP/HTTP以及文件I/O的处理是必须的。我想就是所以在Node.js里面提供了Buffer类处理二进制的数据,能够处理各类类型的数据。数据库

Buffer模块的一个说明。segmentfault

在Node.js里面一些重要模块net、http、fs中的数据传输以及处理都有Buffer的身影,由于一些基础的核心模块都要依赖Buffer,因此在node启动的时候,就已经加载了Buffer,咱们能够在全局下面直接使用Buffer,无需经过require()。且 Buffer 的大小在建立时肯定,没法调整。

Buffer建立

NodeJS v6.0.0版本以前,Buffer实例是经过 Buffer 构造函数建立的,即便用 new 关键字建立,它根据提供的参数返回不一样的 Buffer,但在以后的版本中这种声明方式就被废弃了,替代 new 的建立方式主要有如下几种。后端

1. Buffer.alloc 和 Buffer.allocUnsafe(建立固定大小的buffer)

Buffer.allocBuffer.allocUnsafe 建立 Buffer 的传参方式相同,参数为建立 Buffer 的长度,数值类型。

// Buffer.alloc 和 Buffer.allocUnsafe 建立 Buffer
// Buffer.alloc 建立 Buffer,建立一个大小为6字节的空buffer,通过了初始化
let buf1 = Buffer.alloc(6);

// Buffer.allocUnsafe 建立 Buffer,建立一个大小为6字节的buffer,未通过初始化
let buf2 = Buffer.allocUnsafe(6);

console.log(buf1); // <Buffer 00 00 00 00 00 00>
console.log(buf2); // <Buffer 00 e7 8f a0 00 00>

经过代码能够看出,用 Buffer.allocBuffer.allocUnsafe 建立 Buffer 是有区别的,Buffer.alloc 建立的 Buffer 是被初始化过的,即 Buffer 的每一项都用 00 填充,而 Buffer.allocUnsafe 建立的 Buffer 并无通过初始化,在内存中只要有闲置的 Buffer 就直接 “抓过来” 使用。

Buffer.allocUnsafe 建立 Buffer 使得内存的分配很是快,但已分配的内存段可能包含潜在的敏感数据,有明显性能优点的同时又是不安全的,因此使用需格外 “当心”。

二、Buffer.from(根据内容直接建立Buffer)

Buffer.from(str, )
支持三种传参方式:
  • 第一个参数为字符串,第二个参数为字符编码,如 ASCIIUTF-8Base64 等等。
  • 传入一个数组,数组的每一项会以十六进制存储为 Buffer 的每一项。
  • 传入一个 Buffer,会将 Buffer 的每一项做为新返回 Buffer 的每一项。

说明:Buffer目前支持的编码格式

  • ascii - 仅支持7位ASCII数据。
  • utf8 - 多字节编码的Unicode字符
  • utf16le - 2或4个字节,小端编码的Unicode字符
  • base64 - Base64字符串编码
  • binary - 二进制编码。
  • hex - 将每一个字节编码为两个十六进制字符。
传入字符串和字符编码:
// 传入字符串和字符编码
let buf = Buffer.from("hello", "utf8");

console.log(buf); // <Buffer 68 65 6c 6c 6f>
传入数组:
// 数组成员为十进制数
let buf = Buffer.from([1, 2, 3]);

console.log(buf); // <Buffer 01 02 03>
// 数组成员为十六进制数
let buf = Buffer.from([0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd]);

console.log(buf); // <Buffer e4 bd a0 e5 a5 bd>
console.log(buf.toString("utf8")); // 你好

NodeJS 中不支持 GB2312 编码,默认支持 UTF-8,在 GB2312 中,一个汉字占两个字节,而在 UTF-8 中,一个汉字占三个字节,因此上面 “你好” 的 Buffer 为 6 个十六进制数组成。

// 数组成员为字符串类型的数字
let buf = Buffer.from(["1", "2", "3"]);
console.log(buf); // <Buffer 01 02 03>

传入的数组成员能够是任何进制的数值,当成员为字符串的时候,若是值是数字会被自动识别成数值类型,若是值不是数字或成员为是其余非数值类型的数据,该成员会被初始化为 00。

建立的 Buffer 能够经过 toString 方法直接指定编码进行转换,默认编码为 UTF-8

传入 Buffer:
// 传入一个 Buffer
let buf1 = Buffer.from("hello", "utf8");

let buf2 = Buffer.from(buf1);

console.log(buf1); // <Buffer 68 65 6c 6c 6f>
console.log(buf2); // <Buffer 68 65 6c 6c 6f>
console.log(buf1 === buf2); // false
console.log(buf1[0] === buf2[0]); // true
buf1[1]=12;
console.log(buf1); // <Buffer 68 0c 6c 6c 6f>
console.log(buf2); // <Buffer 68 65 6c 6c 6f>

当传入的参数为一个 Buffer 的时候,会建立一个新的 Buffer 并复制上面的每个成员。

Buffer 为引用类型,一个 Buffer 复制了另外一个 Buffer 的成员,当其中一个 Buffer 复制的成员有更改,另外一个 Buffer 对应的成员不会跟着改变,说明传入buffer建立新的Buffer的时候是一个深拷贝的过程。

Buffer的内存分配机制

buffer对应于 V8 堆内存以外的一块原始内存

Buffer是一个典型的javascriptC++结合的模块,与性能有关的用C++来实现,javascript 负责衔接和提供接口。Buffer所占的内存不是V8堆内存,是独立于V8堆内存以外的内存,经过C++层面实现内存申请(能够说真正的内存是C++层面提供的)、javascript 分配内存(能够说JavaScript层面只是使用它)。Buffer在分配内存最终是使用ArrayBuffer对象做为载体。简单点而言, 就是Buffer模块使用v8::ArrayBuffer分配一片内存,经过TypedArray中的v8::Uint8Array来去写数据。

内存分配的8K机制

  • 分配小内存

说道Buffer的内存分配就不得不说Buffer8KB的问题,对应buffer.js源码里面的处理以下:

Buffer.poolSize = 8 * 1024;

function allocate(size)
{
    if(size <= 0 )
        return new FastBuffer();
    if(size < Buffer.poolSize >>> 1 )
        if(size > poolSize - poolOffset)
            createPool();
        var b = allocPool.slice(poolOffset,poolOffset + size);
        poolOffset += size;
        alignPool();
        return b
    } else {
        return createUnsafeBuffer(size);
    }
}

源码直接看来就是以8KB做为界限,若是写入的数据大于8KB一半的话直接则直接去分配内存,若是小于4KB的话则从当前分配池里面判断是否够空间放下当前存储的数据,若是不够则从新去申请8KB的内存空间,把数据存储到新申请的空间里面,若是足够写入则直接写入数据到内存空间里面,下图为其内存分配策略。

Buffer内存分配策略图
看内存分配策略图,若是当前存储了2KB的数据,后面要存储5KB大小数据的时候分配池判断所需内存空间大于4KB,则会去从新申请内存空间来存储5KB数据而且分配池的当前偏移指针也是指向新申请的内存空间,这时候就以前剩余的6KB(8KB-2KB)内存空间就会被搁置。至于为何会用8KB做为存储单元分配,为何大于8KB按照大内存分配策略,在下面Buffer内存分配机制优势有说明。

  • 分配大内存

仍是看上面那张内存分配图,若是须要超过8KBBuffer对象,将会直接分配一个SlowBuffer对象做为基础单元,这个基础单元将会被这个大Buffer对象独占。

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

这里的SlowBUffer类实在C++中定义的,虽然引用buffer模块能够访问到它,可是不推荐直接操做它,而是用Buffer替代。这里内部parent属性指向的SlowBuffer对象来自Node自身C++中的定义,是C++层面的Buffer对象,所用内存不在V8的堆中

  • 内存分配的限制

此外,Buffer单次的内存分配也有限制,而这个限制根据不一样操做系统而不一样,而这个限制能够看到node_buffer.h里面

static const unsigned int kMaxLength =
    sizeof(int32_t) == sizeof(intptr_t) ? 0x3fffffff : 0x7fffffff;

对于32位的操做系统单次可最大分配的内存为1G,对于64位或者更高的为2G。

buffer内存分配机制优势

Buffer真正的内存实在NodeC++层面提供的,JavaScript层面只是使用它。当进行小而频繁的Buffer操做时,采用的是8KB为一个单元的机制进行预先申请和过后分配,使得Javascript到操做系统之间没必要有过多的内存申请方面的系统调用。对于大块的Buffer而言(大于8KB),则直接使用C++层面提供的内存,则无需细腻的分配操做。

Buffer与stream

stream的流动为何要使用二进制Buffer

根据最初代码的打印结果,stream中流动的数据就是Buffer类型,也就是二进制

缘由一:

node官方使用二进制做为数据流动确定是考虑过不少,好比在上一篇 想学Node.js,stream先有必要搞清楚文章已经说过,stream主要的设计目的——是为了优化IO操做文件IO网络IO),对应后端不管是文件IO仍是网络IO,其中包含的数据格式都是未知的,有多是字符串,音频,视频,网络包等等,即便就是字符串,它的编码格式也是未知的,可能ASC编码,也可能utf-8编码,对于这些未知的状况,还不如直接使用最通用的格式二进制.

缘由二:

Buffer对于http请求也会带来性能提高。

举一个例子:

const http = require('http');
const fs = require('fs');
const path = require('path');

const server = http.createServer(function (req, res) {
    const fileName = path.resolve(__dirname, 'buffer-test.txt');
    fs.readFile(fileName, function (err, data) {
        res.end(data)   // 测试1 :直接返回二进制数据
        // res.end(data.toString())  // 测试2 :返回字符串数据
    });
});
server.listen(8000);

将代码中的buffer-test文件大小增长到50KB左右,而后使用ab工具测试一下性能,你会发现不管是从吞吐量(Requests per second)仍是链接时间上,返回二进制格式比返回字符串格式效率提升不少。为什么字符串格式效率低?—— 由于网络请求的数据原本就是二进制格式传输,虽然代码中写的是 response 返回字符串,最终还得再转换为二进制进行传输,多了一步操做,效率固然低了。

Buffer在stream数据流转充当的角色

咱们能够把整个流(stream)Buffer的配合过程看做公交站。在一些公交站,公交车在没有装满乘客前是不会发车的,或者在特定的时刻才会发车。固然,乘客也可能在不一样的时间,人流量大小也会有所不一样,有人多的时候,有人少的时候,乘客公交车站都没法控制人流量。

不论什么时候,早到的乘客都必须等待,直到公交车接到指令能够发车。当乘客到站,发现公交车已经装满,或者已经开走,他就必须等待下一班车次。

总之,这里总会有一个等待的地方,这个等待的区域就是Node.js中的BufferNode.js不能控制数据何时传输过来,传输速度,就好像公交车站没法控制人流量同样。他只能决定何时发送数据(公交车发车)。若是时间还不到,那么Node.js就会把数据放入Buffer等待区域中,一个在RAM中的地址,直到把他们发送出去进行处理。

注意点:

Buffer虽好也不要瞎用,BufferString二者均可以存储字符串类型的数据,可是,StringBuffer不一样,在内存分配上面,String直接使用v8堆存储,不用通过c++堆外分配内存,而且Google也对String进行优化,在实际的拼接测速对比中,StringBuffer快。可是Buffer的出现是为了处理二进制以及其余非Unicode编码的数据,因此在处理非utf8数据的时候须要使用到Buffer来处理。

今天就分享这么多,若是对分享的内容感兴趣,能够关注公众号「程序员成长指北」,或者加入技术交流群,你们一块儿讨论。

Node系列原创文章:

深刻理解Node.js 中的进程与线程

想学Node.js,stream先有必要搞清楚

require时,exports和module.exports的区别你真的懂吗

源码解读一文完全搞懂Events模块

Node.js 高级进阶之 fs 文件模块学习

关注我

以为不错点个Star,欢迎 加群 互相学习。

做者简介:koala,专一完整的 Node.js 技术栈分享,从 JavaScript 到 Node.js,再到后端数据库,祝您成为优秀的高级 Node.js 工程师。【程序员成长指北】做者,Github 博客开源项目 https://github.com/koala-coding/goodBlog
相关文章
相关标签/搜索