非直接缓冲区,缓冲区建立在JVM内存中,实际读写数据时,需要在OS和JVM之间进行数据拷贝,如下图:
为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?这样做有几个问题。首先,硬件通常不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色
直接缓冲区,缓冲区建立在受操作系统管理的物理内存中,OS和JVM直接通过这块物理内存进行交互,没有了中间的拷贝环节,如下图:
所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件RAM)内存地址。这样做好处颇多,总结起来可分为两大类: 1. 一个以上的虚拟地址可指向同一个物理内存地址。 2. 虚拟内存空间可大于实际可用的硬件内存。
前一节提到,设备控制器不能通过 DMA 直接存储到用户空间,但通过利用上面提到的第一项,则可以达到相同效果。把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,这样,DMA 硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区(见图1-3)。
优点:速度更快,效率更高
缺点:①创建直接缓冲区将会有更多消耗②数据进入直接缓冲区后,后续写入磁盘等操作就完全由操作系统决定了,不受我们控制
什么时候用:缓冲区要长时间使用(数据本身需要长时间在内存
OR 缓冲区复用率很高),或者大数据量的操作(大文件才能体现出速度优势)。
如何使用:
- 通过
ByteBuffer.allocateDirect()
,创建直接缓冲区 - 通过内存映射文件的方式
- 使用通道直接传输
下面以2G大小的文件,对比一下
非直接缓冲区完成文件复制
public void testChannel() throws Exception{
long begin = System.currentTimeMillis();
FileInputStream fis = new FileInputStream("1.zip");
FileOutputStream fos = new FileOutputStream("2.zip");
//获取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
//创建Buffer
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
//将数据写入byteBuffer
while(inChannel.read(byteBuffer) != -1){
//转换模式
byteBuffer.flip();
//将byteBuffer中的数据读取到outChannel
//注意这里的byteBuffer是有position limit属性的,write时候只会写position->limit之间的数据
//所以,即使clear()并未真正清空buffer,这里也不会把上次的数据写入的
outChannel.write(byteBuffer);
//清空byteBuffer
byteBuffer.clear();
}
outChannel.close();
inChannel.close();
fos.close();
fis.close();
System.out.println(System.currentTimeMillis() - begin);
}
直接缓冲区完成文件复制
与上述代码一样,只是将创建缓冲区的方式改成ByteBuffer.allocateDirect()
内存映射文件方式
①原视频中直接将1G左右的文件一次性通过mapBuffer进行传输,这样比较的结果可能不准确,因为非直接缓冲区一次只读取1024个字节,所以我这里也改成了一次读1024个字节
②我用的是2G的文件,本来想试试一次读2G和多次读差别多大,结果直接报堆溢出,不是都直接操作物理内存了嘛,咋还跟JVM堆有关,这可能涉及到更深的内容,而且NIO重点也不在这块儿,所以这里就暂将此问题挂起,不深入研究了
public void testChannel2() throws Exception{
long begin = System.currentTimeMillis();
//另一种方式获取Channel
FileChannel inChannel = FileChannel.open(Paths.get("1.zip"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("3.zip"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//内存映射文件
MappedByteBuffer inMapBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接对映射缓冲区进行读写,不需要ByteBuffer
//这里不能直接传2g,否则会报错堆溢出,要分开传
byte [] tempBytes = new byte[1024];
while(inMapBuffer.hasRemaining()){
int shouldReadLength = inMapBuffer.remaining() > 1024?1024:inMapBuffer.remaining();
//写入bytes
inMapBuffer.get(tempBytes,0,shouldReadLength);
//从bytes读出到outMapBuffer
outMapBuffer.put(tempBytes,0,shouldReadLength);
}
outChannel.close();
inChannel.close();
System.out.println(System.currentTimeMillis() - begin);
}
通道直接传输
只有FileChannel有这个方式,不需要借助缓冲区
public void testChannel3() throws Exception{
long begin = System.currentTimeMillis();
FileChannel inChannel = FileChannel.open(Paths.get("1.zip"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("3.zip"), StandardOpenOption.READ,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//这里传2g的也不会报堆溢出
inChannel.transferTo(0, inChannel.size(), outChannel);
//outChannel.transferFrom(inChannel, 0, inChannel.size());
outChannel.close();
inChannel.close();
System.out.println(System.currentTimeMillis() - begin);
}
到这里应该能明白NIO与IO的第一个区别了吧:
NIO是面向缓冲区,基于通道进行IO操作,能以更加高效的方式进行文件的读取操作