目录
NIO概述
Java NIO(New I/O,也有称为Non-Blocking IO)在jdk1.4提出,目的在于提高IO的执行效率。NIO基于Channel通道以及Buffer缓冲区来实现I/O操作,其 I/O速度的提高在于把数据的填充和提取放回了操作系统中去执行。例如在Java IO详细总结(一篇涵盖所有)一文中提到为了提供速度用Buffer来装饰原有的I/O,但是不管是在读取数据源数据并将数据放到缓冲区,还是把缓冲区中的数据写入到数据源。这两种操作都是在程序中执行的,而NIO则把上述的操作放回了操作系统因此速度有所提高。此外,本文只讨论文件I/O。
下面是Java NIO类的思维导图:
I/O与NIO区别
- I/O面向流操作每次执行单个字节,NIO面向缓冲Buffer每次操作的的数据由Buffer大小来决定;
- I/O中缓冲区数据的读取或者写入在程序中实现,NIO则是允许在操作系统中完成;
- NIO非阻塞,IO是阻塞的。
FileChannel和ByteBuffer的使用
首先通过一个简单的例子:NIOTest,来了解FileChannel以及ByteBuffer是如何使用的:
// NIOTest.java
try (FileChannel out = new FileOutputStream(new File("D://test.txt")).getChannel();
FileChannel in = new FileInputStream(new File("D://test.txt")).getChannel()) {
//ByteBuffer bb = ByteBuffer.allocateDirect(3);
ByteBuffer bb = ByteBuffer.allocate(10);
// out.write(ByteBuffer.wrap("abc测试".getBytes()));
bb.put("abc测试".getBytes());
bb.flip();
out.write(bb);
bb = ByteBuffer.allocate(10);
// 到达文件末尾返回-1
for (;in.read(bb) != - 1;) {
bb.flip();
// 指定字符集对缓冲区解码
System.out.println(Charset.forName("UTF-8").decode(bb));
bb.clear();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
输出:abc测试
上例主要由下面几步来完成文件的输入输出:
- 打开文件对应的读写通道FileChannel;
- 创建缓冲区ByteBuffer;
- 程序填充数据到ByteBuffer,FileChannel写入;
- FileChannel一直读取数据到ByteBuffer中,直到文件末尾;
- 由于用字节写入,打印到控制台的时候通过Charset指定字符集使其能正常显示。
后文将根据源码来了解ByteBuffer以及FileChannel是如果完成NIOTest中的读写过程的。
ByteBuffer
创建ByteBuffer对象
ByteBuffer是一个抽象类不能实例化,因此不能通过显示的new来创建对象。创建ByteBuffer有两种方式:一种是直接缓冲区,通过调用allocateDirect来实现;一种是非直接缓冲区通过allocate来实现。直接缓冲区的优势在于针对其上的I/O操作都在操作系统上执行,相对的非直接缓冲区需要把缓冲区的数据复制到一块中间缓冲区再把中间缓冲区的数据写入内存。很明显前者的速度会快于后者。直接缓冲区开辟的内存空间称为直接内存不在JVM管理中,其不受GC的影响所以文档中建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
非直接缓冲区:通过allocate、wrap方法;
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
/*
hb = new byte[cap];
offset = 0;
*/
}
public static ByteBuffer wrap(byte[] array) {
return wrap(array, 0, array.length);
}
public static ByteBuffer wrap(byte[] array, int offset, int length)
{
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
从源码可以看到当ByteBuffer调用allocate方法时创建了一个新的对象HeapByteBuffer,该对象继承了ByteBuffer,通过该对象调用父类构造器进行初始化返回子类实例并上转成父类。值得一提的是wrap方法一样可以返回HeapByteBuffer。两者的区别在于是否wrap直接把要写入的数据直接赋值给ByteBuffer底层数据,在本例中相较allocate省去了为底层数组赋值这一步骤。
直接缓冲区:allocateDirect方法
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
可以看到allocateDirect跟allocate方式完全不同,前者通过unsafe类直接向操作系统申请了一块内存(在虚拟机上这块内存称为直接内存)并且后续对缓冲区的所有操作都是在内存上直接进行。此外,如果申请的内存过大会抛出内存溢出。
文章后续在介绍ByteBuffer特性的时候会针对HeapByteBuffer和DirectByteBuffer两种情况进行说明。