当前位置: 首页 > 后端技术 > Node.js

探索V8堆内存中没有存储的Buffer对象

时间:2023-04-03 15:46:57 Node.js

前言写完上一篇,想学习Node.js。需要先弄清楚流对象的悬念。流对象数据流的具体内容是什么?本文将为您深入解读。Buffer之前探索了一个使用stream操作文件的例子:varfileName=path.resolve(__dirname,'data.txt');varstream=fs.createReadStream(文件名);console.log('流内容',stream);stream.on('data',function(chunk){console.log(chunkinstanceofBuffer)console.log(chunk);})看一下打印结果,发现第一个stream是一个对象,取部分内容截图。第二个和第三个打印结果是Buffer对象,类似于数组,其元素是两位十六进制数,即0到255之间的值。可以看出流中流动的数据是Buffer类型,二进制数据,接下来我们就开始我们的Buffer探索之旅。什么是二进制?二进制是计算机的最低级数据格式。字符串、数字、视频、音频、程序、网络包等都在最底层以二进制存储。这些高级格式和二进制格式可以通过固定的编码格式相互转换。例如C语言中int32类型的十进制整数(无符号)占32位即4个字节,十进制中3对应的二进制值为00000000000000000000000000000011。字符串也一样,可以转化为和根据ASCII编码规则或unicode编码规则(如utf-8)从二进制中提取。简而言之,计算机底层存储的数据都是二进制格式,各种高级类型都有相应的编码规则和二进制转换。为什么Buffer模块会出现在node中?在原有的javascript生态中,javascript仍然运行在浏览器端。处理Unicode编码的字符串数据很容易,但处理二进制和非Unicode编码的数据就无能为力了。但对于服务器端,需要处理TCP/HTTP和文件I/O。我想这就是Node.js中提供Buffer类来处理二进制数据的原因,它可以处理各种类型的数据。缓冲模块的描述。在Node.js中,一些重要的模块net、http、fs在Buffer中进行数据传输和处理,因为一些基础核心模块依赖于Buffer,所以Buffer在node启动的时候就已经加载好了,我们在全局下直接使用Buffer就可以不用要求()。而Buffer的大小在创建的时候就确定了,不能调整。Buffer是在NodeJSv6.0.0之前创建的,Buffer实例是通过Buffer构造函数创建的,即使用new关键字创建,根据提供的参数返回不同的Buffer,但是这种声明方式在之后的版本中被废弃了现在,主要有以下几种方式来创建alternativenew。1、Buffer.alloc和Buffer.allocUnsafe(创建一个固定大小的缓冲区)使用Buffer.alloc和Buffer.allocUnsafe以同样的方式创建一个Buffer。参数是创建的Buffer的长度,以及值的类型。//Buffer.alloc和Buffer.allocUnsafe创建Buffer//Buffer.alloc创建一个Buffer,创建一个大小为6字节的空缓冲区,初始化后letbuf1=Buffer.alloc(6);//Buffer.allocUnsafe创建一个Buffer,创建一个大小为6字节的缓冲区,未初始化letbuf2=Buffer.allocUnsafe(6);console.log(buf1);//<缓冲区000000000000>console.log(buf2);//从代码中可以看出,使用Buffer.alloc和Buffer.allocUnsafe创建Buffer是有区别的。Buffer.alloc创建的Buffer是初始化的,即Buffer的每一项都是用00填充的,而Buffer.allocUnsafe创建的Buffer还没有初始化,所以只要内存中有空闲的Buffer,就会被“抢”过来直接使用。Buffer.allocUnsafe创建一个Buffer,使得内存的分配非常快,但是分配的内存段可能包含潜在的敏感数据,具有明显的性能优势,并且不安全,因此需要格外小心使用。2、Buffer.from(直接根据内容创建Buffer)Buffer.from(str,)支持三种参数传递方式:第一个参数为字符串,第二个参数为字符编码,如ASCII、UTF-8,Base64等。传入一个数组,数组的每一项都会以16进制的形式存储为Buffer的每一项。传递Buffer将返回Buffer的每个项目作为新Buffer的每个项目。注意:Buffer目前支持编码格式ascii——只支持7位ASCII数据。utf8-Unicode字符的多字节编码utf16le-2或4字节,Unicode字符的小端编码base64-Base64字符串编码binary-二进制编码。hex-将每个字节编码为两个十六进制字符。传入的字符串和字符编码://传入的字符串和字符编码letbuf=Buffer.from("hello","utf8");console.log(buf);//传入数组://数组成员是十进制数letbuf=Buffer.from([1,2,3]);控制台日志(buf);////数组成员为十六进制Letbuf=Buffer.from([0xe4,0xbd,0xa0,0xe5,0xa5,0xbd]);控制台日志(buf);//console.log(buf.toString("utf8"));//你好,NodeJS不支持GB2312编码,默认支持UTF-8。在GB2312中,一个汉字占两个字节,而在UTF-8中,一个汉字占三个字节,所以上面的“Hello”Buffer是由6个十六进制数组成的。//数组成员是字符串类型的数字letbuf=Buffer.from(["1","2","3"]);控制台日志(buf);//传入的数组成员可以是任意数值。当成员为字符串时,如果值为数字,则自动识别为数字类型。如果value不是数字或者成员是其他非数字数据,成员会被初始化为00。创建的Buffer可以通过toString方法直接指定编码进行转换,默认编码为UTF-8。传入Buffer://传入一个Bufferletbuf1=Buffer.from("hello","utf8");letbuf2=Buffer.from(buf1);console.log(buf1);//<缓冲区68656c6c6f>console.log(buf2);//<缓冲区68656c6c6f>console.log(buf1===buf2);//falseconsole.log(buf1[0]===buf2[0]);//truebuf1[1]=12;console.log(buf1);//<缓冲区680c6c6c6f>console.log(buf2);//当传入参数为Buffer时,创建一个新的Buffer并复制上面的每个成员。缓冲区是引用类型。一个Buffer复制另一个Buffer的成员。当一个Buffer的复制成员改变时,另一个Buffer对应的成员不会改变。意思是当传入buffer创建一个新的Buffer时,是一个深拷贝。的过程。Buffer的内存分配机制buffer对应的是V8堆内存之外的一块原始内存。Buffer是javascript和C++结合的典型模块。性能相关的功能用C++实现,javascript负责连接和提供接口。Buffer占用的内存不是V8堆内存,而是独立于V8堆内存的内存。内存申请是通过C++层级实现的(可以说真正的内存是C++层级提供的),javascript分配内存(可以说JavaScript层级只用)。Buffer在分配内存的时候最终还是以ArrayBuffer对象为载体。简单来说,Buffer模块使用v8::ArrayBuffer分配一块内存,通过v8::Uint8Array向TypedArray写入数据。内存分配的8K机制分配小内存。说到Buffer内存分配,就不得不说到Buffer的8KB问题。buffer.js源码中对应的处理如下:Buffer.poolSize=8*1024;functionallocate(size){if(size<=0)returnnewFastBuffer();if(size>>1)if(size>poolSize-poolOffset)createPool();varb=allocPool.slice(poolOffset,poolOffset+size);poolOffset+=大小;对齐池();returnb}else{returncreateUnsafeBuffer(大小);}}源码直接看8KB为限制,如果写入的数据大于8KB的一半,则直接分配内存,如果小于4KB则从当前分配池判断当前是否有足够的空间存储的数据,如果没有,重新申请8KB的内存空间,将数据存储在新申请的空间中,如果有足够的空间可以写入,则直接将数据写入内存空间,下图是它的内存分配策略。看内存分配策略图,如果当前存储了2KB的数据,后面要存储5KB的数据时,分配池判断需要的内存空间大于4KB,就会重新申请内存空间存储5KB的数据和分配池当前偏移量的指针也指向新申请的内存空间,此时,剩余的6KB(8KB-2KB)内存空间将被搁置。至于为什么使用8KB作为存储单元分配,以及根据大内存分配策略为什么大于8KB,下面会解释Buffer内存分配机制的优点。要分配大内存,请参见上面的内存分配图。如果需要超过8KB的Buffer对象,会直接分配一个SlowBuffer对象作为基本单元,这个基本单元会被这个大Buffer对象独占。//大缓冲区,只需分配onethis.parent=newSlowBuffer(this.length);this.offset=0;这里的SlowBUffer类其实是用C++定义的。虽然可以通过引用buffer模块来访问,但是不建议直接操作,而是使用Buffer。这里的internalparent属性所指向的SlowBuffer对象来自于Node自带的C++中的定义。它是C++级别的Buffer对象。使用的内存不受V8堆中内存分配的限制。另外,Buffer的单内存分配也有限制,这个限制根据不同的操作系统不同,这个限制可以在node_buffer.h中看到staticconstunsignedintkMaxLength=sizeof(int32_t)==大小(intptr_t)?0x3fffffff:0x7fffffff;对于32位操作系统,一次可以分配的最大内存为1G,64位或更高版本为2G。Buffer内存分配机制的优点Buffer的真实内存其实是Node的C++层面提供的,JavaScript层面只是使用而已。在进行小而频繁的Buffer操作时,采用8KB为单位的机制进行预申请和后分配,这样Javascript和操作系统之间就不需要过多的内存申请系统调用。对于大缓冲区(大于8KB),直接使用C++层提供的内存,不需要精细的分配操作。Buffer和stream为什么要用binaryBuffer根据原代码的打印结果,stream中流动的数据是Buffer类型,是二进制的。原因一:Node官方使用二进制作为数据流,想必是经过深思熟虑的。比如上一篇想要学习Node.js,首先要看懂文章。文中提到,stream的主要设计目的是为了优化IO操作。(文件IO和网络IO),对应的后端是文件IO还是网络IO,里面包含的数据格式是未知的,可能是字符串,音频,视频,网络包等,即使是字符串,它的编码格式也是未知的,可能是ASC编码,也可能是utf-8编码。对于这些未知的情况,最好直接使用最常见的二进制格式。原因二:Buffer对于http请求也会提升性能。举个例子:consthttp=require('http');constfs=require('fs');constpath=require('路径');constserver=http.createServer(function(req,res){constfileName=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工具进行性能测试。你会发现在吞吐量(每秒请求数)和连接时间方面,返回二进制格式比返回字符串格式要高效得多。为什么字符串格式化效率低下?——因为网络请求的数据本来就是以二进制格式传输的,虽然在代码中写了响应字符串,但最后还是要转成二进制传输,这就需要多一步操作,当然还有效率低下。Buffer在流数据流动中的作用我们可以把stream(流)和Buffer的整个协作过程看成一个公交车站。在一些公交车站,公交车要坐满乘客才会发车,或者只在特定时间发车。当然,乘客在不同时间也可能有不同的人流。人多的时候,人少的时候,无论是乘客还是公交车站都无法控制人流。在任何时候,早到的乘客都必须等到公交车获准离开。当乘客到达车站,发现公共汽车已满或已经离开时,他必须等待下一班公共汽车。简而言之,这里总是有一个等待的地方。这个等待区就是Node.js中的Buffer。Node.js无法控制何时传输数据和传输速度,就像公交车站无法控制人流一样。他只能决定何时发送数据(总线出发)。如果时间还没到,那么Node.js会把数据放到Buffer等待区,也就是RAM中的一个地址,直到被送出去处理。注意:Buffer虽然好,但不要乱用。Buffer和String都可以存储字符串类型的数据。但是,String与Buffer不同。在内存分配上,String直接存储在v8堆中,没有经过C++堆分配。Memory,Google也对String进行了优化。在实际的拼接速度对比中,String要快于Buffer。但是Buffer的出现是为了处理二进制等非Unicode编码的数据,所以需要使用Buffer来处理非utf8数据。今天就分享这么多,如果你对分享的内容感兴趣,可以关注公众号《程序员成长指南》,或者加入技术交流群,一起探讨。Node系列原创文章:深入理解Node.js中的进程和线程想要学习Node.js和stream,首先要搞清楚require时exports和module.exports的区别。源码解读你真的懂吗?Node.js进阶进阶fs文件模块学习注意我觉得不错点个star,欢迎进群互相学习。作者简介:考拉,专注分享完整的Node.js技术栈,从JavaScript到Node.js,再到后端数据库,祝你成为优秀的资深Node.js工程师。【程序员的成长指南】作者,Github博客开源项目https://github.com/koala-coding/goodBlog