写完第三章之后,发现第一章中关于内核缓冲区的内容有错误,不好意思,已修复
如果这篇文章对您有些用处,请点赞告诉我O(∩_∩)O
一、概述
1、JAVA NIO,首先要理解NIO 是指 New IO,2002年在JDK1.4中引入。并不是Non-blocking I/O非阻塞IO的意思,包名为java.nio。
而”传统“IO则是指在此之前就已经存在java.io包中的IO类。
2、NIO和传统IO比较,从JAVA NIO API开头就可以看到核心抽象类:
(1)Buffer:数据容器
(2)Charset:字节与字符之间的转换所需要的编码解码。
(3)Channel:使用通道(连接到本地存储或网络)执行各种IO操作。
(4)Selector:能够一次选择多个通道,支持多路复用以及非阻塞IO的能力。
3、看不明白,没有关系,这些概念后面逐个说明。让我们先瞥一眼,提出一些问题,带着问题看下去:
(1)传统IO中也有BufferInputStream,BufferReader,和NIO中的Buffer相比有什么不同?
(2)传统IO中的Reader,Writer也能实现编码解码,和NIO中的Charset相比有什么不同?
(3)传统IO对流read,writer,NIO对通道read,writer有什么不同?是否支持一些新的IO操作?
(4)传统IO里面没有Selector这个特性,看不懂,它能带来什么好处?
(准备用四个章节回答这四个问题,今天请关注第一个问题)
二、基本概念
1、Buffer 数据容器
Buffer本质和数组一样,在内存中的一段连续空间,用来存储同一种类型的数据。
主要不同之处在于内存分配方式和索引管理,数组 VS Buffer,如下:
(mark与reset不是本文重点,与RandomAccessFile做对比的内容会补充在”最后的最后“)
//在堆中申请20字节空间, capacity=20, limit=20, position=0
ByteBuffer bb = ByteBuffer.allocate(20);
//向Buffer写入9个字节,capacity=20, limit=20, position=9
bb.put("Hello NIO".getBytes());
//调整limit为当前position并且position归零, 准备从Buffer读出,capacity=20, limit=9, position=0
bb.flip();
//从Buffer中逐个读出字节,直到postion=limit
while (bb.hasRemaining()) {
System.out.print(buffer.get() + " "); //每读出一个字节 position+=1
}
2、Channel通道
Channel表示可以进行多种IO操作的连接(到文件、socket或其他硬件设备)。传统IO操作流,NIO操作Channel。
三、从文件中读取数据的三种方式
1、传统IO-BufferedInputStream
byte[] bs = new byte[BSIZE];
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(FILE))) {
while (bis.read(bs) != -1) {
// System.out.println(Arrays.toString(bs));
}
} catch (IOException e) {
e.printStackTrace();
}
说明:
(1)磁盘文件->内核缓冲区,由操作系统内核完成。
(2)read系统调用将内核缓冲区的数据拷贝到用户空间中的临时空间,再由临时空间拷贝到BufferedInputStream中的buf[]数组中,最后释放临时空间,由C程序完成。
(3)从BufferedInputStream的buf设 拷贝到 bs输出,由JAVA完成。
(4)过程中数据拷贝4次。
2、NIO-HeapByteBuffer
ByteBuffer bb = ByteBuffer.allocate(BSIZE);
try (FileChannel fc = new FileInputStream(FILE).getChannel()) {
while (fc.read(bb) != -1) {
bb.flip();
// System.out.println(Arrays.toString(bb.array()));
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
说明:
(1)ByteBuffer.allocate方法创建HeapByteBuffer,使用new byte[1024]分配堆内存,hb为引用。
(2)当读取文件时,先使用ByteBuffer.allocateDirect分配临时堆外内存,再从文件(FileDescriptor)读到临时堆外内存。
(3)然后将临时堆外内存 逐个字节拷贝到 ByteBuffer中的hb中。
(4)最后释放临时堆外内存。
(5)过程中数据拷贝3次。
3。NIO -DirectByteBuffer
ByteBuffer bb = ByteBuffer.allocateDirect(BSIZE);
try (FileChannel fc = new FileInputStream(FILE).getChannel()) {
while (fc.read(bb) != -1) {
bb.flip();
//注DirectByteBuffer内hb为空,不支持bb.array()
// while(bb.hasRemaining()) {
// System.out.print(bb.get() + " ");
// }
bb.clear();
}
bb.flip();
} catch (IOException e) {
e.printStackTrace();
} finally {
((DirectBuffer) bb).cleaner().clean(); //手动清除堆外内存
}
说明:
(1)ByteBuffer.allocateDirect方法创建DirectByteBuffer,使用unsafe.allocateMemory分配JVM堆外内存。
(2)当读取文件时,使用read系统调用,从文件(FileDescriptor)读到上一步申请的堆外内存中。
(3)当使用bb.get()方法时,则直接从堆外内存中读取字节(native方法getByte)。
(4)最后注意分配的JVM堆外内存自然不能被GC,用完后需要手动释放((DirectBuffer) bb).cleaner().clean()。
(5)过程中数据拷贝2次。
四、解答问题
(1)传统IO中也有BufferInputStream,BufferReader,和NIO中的Buffer有什么不同?
通过比较三种读取文件的方式,开文中第1个问题,有了答案:
(1)数据拷贝次数减少,BufferedInputStream拷贝了4次,HeapByteBuffer 拷贝了3次,DirectByteBuffer拷贝2次。
(2)BufferedInputStream中的Buffer指的是堆内数组,用作”预读“(默认8K),后面再次读可以从堆内直接读出,直至Buffer被读完。不必每次都通过系统调用读取内核缓冲区。
(3)DirectByteBuffer中的Buffer指的是JVM堆外内存,针对大文件了减少数据拷贝。get/put方法也是直接获取/设置JVM堆外内存中的数据。缺点是JVM堆外内存不受GC管制,需要客户端代码手动释放。
(4)HeapByteBuffer是一个折中选择,先读到临时DirectByteBuffer,再写到HeapByteBuffer内数组,然后释放临时DirectByteBuffer。整个过程HeapByteBuffer控制,客户端代码只用调用FileChannel.read方法即可。
五、性能对比
对大小为1.07G的文件,使用三种方式读取数据,测试性能影响:
随着每次读取字节数量越来越大,BufferInputStream“预读”优势越来越小,HeapByteBuffer和DirectByteBuffer“直接操作内核缓冲区,减少数据拷贝”的优势慢慢展现出来。
由此推断,NIO更适合大块大块(GB级)的读取大文件。
未完待续!