Buffer缓冲区,所谓的缓冲区其实就是在内存中开辟的一段连续空间,用来临时存放数据。
在缓冲区中存在三个基础的游标:capacity(容量)、limit(限制位)、position(当前位)。
当缓冲区刚创建出来时,capacity指向缓冲区的容量即缓冲区的末尾位置,limit等于capacity,position等于0指向最开始的位置。
当向缓冲区写入数据时,会向position指定位置写入数据,并将position+1指向下一个写入位置,为后续接着写入做好准备。而position无论合适都不能大于limit,如果任何写入操作将会导致position大于limit则写入失败抛出异常。
在读取数据时,会将position指向位置中的数据返回,并将position+1指向下一个读取位置,如果任何读取操作造成position大于limit则读取失败,抛出异常。
通常在写完数据要开始读取数据之前要将limit设置为和position相同,指定好边界,再将position设置为0,从头开始读取数据。可以通过flip方法便捷的去实现这个操作。
Mark(标记):下一个要被读或写的元素的索引。位置会自动由相应的get()和put()函数更新。
例如:
初始化缓冲区:
capacity: 10, position: 0, limit: 10
mark: java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
写入缓冲区9个byte:
capacity: 10, position: 9, limit: 10
mark: java.nio.HeapByteBuffer[pos=9 lim=10 cap=10]
使用flip重置元素位置:
capacity: 10, position: 0, limit: 9
mark: java.nio.HeapByteBuffer[pos=0 lim=9 cap=10]
读取元素:1|读取元素:16|读取元素:12|读取元素:0|读取元素:17|读取元素:5|读取元素:4|读取元素:13|读取元素:18|
使用get读取元素后:
capacity: 10, position: 9, limit: 9
mark: java.nio.HeapByteBuffer[pos=9 lim=9 cap=10]
恢复初始化态clear:
capacity: 10, position: 0, limit: 10
mark: java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
利用Buffer读写数据,通常遵循四个步骤:
从Buffer读数据有两种方式:
java.nio.Buffer
Buffer是一个抽象类,不能直接使用,其子类如下:
ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。
此抽象类有以下这些重要的方法。
返回缓冲区的容量大小。
返回缓冲区的当前位置。
返回缓冲区的限制位。
当前position设置标记。
反转缓冲区。将position的值赋给limit,然后position置零。一般用于重新读取数据之前。此方法可以把Buffer从写模式切换到读模式,使用此方法后,position代表的是读取位置,limit标示的是已写入的数据位置。
判断边界,返回当前位置与限制之间的元素数。
告知在当前位置和限制之间是否有元素。
重绕缓冲区。只是将position的数值置零。这样可以重复读取buffer中的数据。limit保持不变。
将此缓冲区的位置重置为mark方法标记的位置。
清空缓冲区。这个方法不是真的清空,而是将三个标志位重置到最初的位置。再写入数据,将原有数据覆盖,是逻辑上的清空,不是物理清空。
判断当前缓冲区是否是直接缓冲区,如果是直接缓冲区,则返回true,否则返回false。
以ByteBuffer进行介绍,其他类型的对应类,都具有相同的功能。
除了继承父类的重要方法之外,ByteBuffer还具有以下重要的方法。
此方法为一个静态方法,创建一个新的指定空间的间接缓冲区。
此方法为一个静态方法,创建一个新的指定容量的直接缓冲区。
此方法为一个静态方法,将byte数组包装到间接缓冲区中。
内部调用的是wrap(byte[] array, int offset, int length)方法。
此方法为一个静态方法,将byte数组包装到间接缓冲区中。
向缓冲区当前position位置写入数据b。
获取当前position位置的数据。
判断两个buffer相对,需满足:
从上面的三个条件可以看出,equals只比较buffer中的部分内容,并不会去比较每一个元素。
public static void main(String[] args) {
//创建指定大小缓冲区
ByteBuffer buf = ByteBuffer.allocate(5);
//写入数据
buf.put("a".getBytes());
buf.put("b".getBytes());
buf.put("c".getBytes());
buf.put("d".getBytes());
buf.put("e".getBytes());
//读取数据
//反转缓冲区 等价于 buf.limit(buf.position()); buf.position(0);
buf.flip();
//判断缓冲区是否到了结尾 buf.remaining() > 0
//--也可以使用 buf.hasRemaining()
while(buf.hasRemaining()){
byte[] data = new byte[1];
buf.get(data);
String str = new String(data);
System.out.println(str);
}
//重绕缓冲区
buf.rewind();
while(buf.hasRemaining()){
byte[] data = new byte[1];
buf.get(data);
String str = new String(data);
System.out.println(str);
}
//mark reset
buf.rewind();
byte[] data = new byte[1];
buf.get(data);
String str = new String(data);
System.out.println(str);
buf.mark();//标记位
buf.get(data);
str=new String(data);
System.out.println(str);
buf.get(data);
str=new String(data);
System.out.println(str);
buf.reset();//返回标记位
buf.get(data);
str=new String(data);
System.out.println(str);
//清除数据
buf.clear();
}
此类继承了ByteBuffer类。
内存映射文件I/O是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的I/O快的多。内存映射文件I/O是通过使文件中的数据出现为内存数组的内容来完成的,不是将整个文件读到内存中,而是只有文件中实际读取或者写入的部分才会映射到内存中。
FileChannel提供了map方法来把文件影射为内存映像文件:
MappedByteBuffer map(int mode,long position,long size);
可以把文件的从position开始的size大小的区域映射为内存映像文件,映射内存缓冲区是个直接缓冲区,继承自ByteBuffer,但相对于ByteBuffer,它有更多的优点:读取快、写入快、随时随地写入。
mode指出了可访问该内存映像文件的方式:
此类出了继承父类的方法之外,还提供了以下这些方法可以使用。
缓冲区是READ_WRITE模式下,此方法对缓冲区内容的修改强行写入文件。
将缓冲区的内容载入内存,并返回该缓冲区的引用。
如果缓冲区的内容在物理内存中,则返回真,否则返回假。
/**
* 通过MappedByteBuffer将数据输出到文件中
*
* @param context 文件内容
* @param filePath 文件名称
* @throws IOException
*/
public static void outputByMap(String context, String filePath) throws IOException {
RandomAccessFile raf = new RandomAccessFile(filePath, "rw");
FileChannel fc = raf.getChannel();
byte[] msg = context.getBytes();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, msg.length);
mbb.put(msg);
raf.close();
}
直接缓冲区DirectByteBuffer:直接在堆外分配一个内存(即,native memory)来存储数据,程序通过JNI直接将数据读/写到堆外内存中。
因为数据直接写入到了堆外内存中,所以这种方式就不会再在JVM管控的堆内再分配内存来存储数据了,也就不存在堆内内存和堆外内存数据拷贝的操作了。这样在进行I/O操作时,只需要将这个堆外内存地址传给JNI的I/O的函数就好了。
底层的数据其实是维护在操作系统的内存中,而不是JVM里,DirectByteBuffer里维护了一个引用address指向了数据,从而操作数据。实现zero copy(零拷贝)。
DirectByteBuffer此类为一个受保护的类,继承了MappedByteBuffer类,不能从外部直接调用,需要通过ByteBuffer的各种子类调用allocateDirect()方法创建对象。
案例:
public static void main(String[] args) throws Exception {
String inFile = "E:\\1.mp4";
String outFile = "D:\\test\\1.mp4";
outputByDirectBuffer(inFile, outFile);
}
/**
* 通过直接缓冲区输出内容到文件。
*
* @param inFile
* @param outFile
* @throws Exception
*/
public static void outputByDirectBuffer(String inFile, String outFile) throws Exception {
long start = System.currentTimeMillis();
FileInputStream is = new FileInputStream(inFile);
FileOutputStream fos = new FileOutputStream(outFile);
FileChannel fcIs, fcOut;
fcIs = is.getChannel();
fcOut = fos.getChannel();
// 创建直接内存的缓冲区
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(2048);
while (fcIs.read(directByteBuffer) != -1) {
directByteBuffer.flip();
fcOut.write(directByteBuffer);
directByteBuffer.clear();
}
is.close();
fos.close();
long end = System.currentTimeMillis();
System.out.println("DirectByteBuffer需要时间:" + (end - start));
}
间接缓冲区HeapByteBuffer:数据的分配存储都在JVM堆上,当需要和IO设备打交道的时候,会将JVM堆上所维护的byte[]拷贝至堆外内存,然后堆外内存直接和IO设备交互。
外设之所以要把JVM堆里的数据copy出来再操作,不是因为操作系统不能直接操作JVM内存,而是因为JVM在进行GC(垃圾回收)时,会对数据进行移动,一旦出现这种问题,外设就会出现数据错乱的情况。
HeapByteBuffer此类为一个受保护的类,继承了MappedByteBuffer类,不能从外部直接调用,需要通过ByteBuffer的各种子类调用allocate()方法创建对象。
案例:
public static void main(String[] args) throws Exception {
String inFile = "E:\\1.mp4";
String outFile = "D:\\test\\1.mp4";
outputByHeapBuffer(inFile, outFile);
}
/**
* 通过间接缓冲区输出内容到文件。
*
* @param inFile
* @param outFile
* @throws Exception
*/
public static void outputByHeapBuffer(String inFile, String outFile) throws Exception {
long start = System.currentTimeMillis();
FileInputStream is = new FileInputStream(inFile);
FileOutputStream fos = new FileOutputStream(outFile);
FileChannel fcIs, fcOut;
fcIs = is.getChannel();
fcOut = fos.getChannel();
// 创建间接缓冲区
ByteBuffer directByteBuffer = ByteBuffer.allocate(2048);
while (fcIs.read(directByteBuffer) != -1) {
directByteBuffer.flip();
fcOut.write(directByteBuffer);
directByteBuffer.clear();
}
is.close();
fos.close();
long end = System.currentTimeMillis();
System.out.println("HeapByteBuffer需要时间:" + (end - start));
}
以上是关于NIO的Buffer的介绍。