NIO基础之Buffer

java.io 核心概念是流,即面向流的编程,在java中一个流只能是输入流或者输出流,不能同时具备两个概念。java

java.nio核心是 selector、Channel、Buffer ,是面向缓冲区(buffer)或者面向块block。编程

1、Buffer 数组

Buffer自己是一个内存块,底层是数组,数据的读写都是经过Buffer类实现的。即同一个Buffer便可以写数据也能够读数据,经过intBuffer.flip()方法进行Buffer位置状态的翻转。JAVA中的8中基本类型都有各自对应的Buffer。app

缓冲区buffer主要是和通道数据交互,即从通道中读入数据到缓冲区,和从缓冲区中把数据写入到通道中,经过这样完成对数据的传输。 它经过几个变量来保存这个数据的当前位置状态。dom

Buffer中的四个核心变量jvm

  • 容量(Capacity):缓冲区可以容纳的数据元素的最大数量。这一个容量在缓冲区建立时被设定,而且永远不能改变。
  • 界限(Limit):指定还有多少数据须要取出(在从缓冲区写入通道时),或者还有多少空间能够放入数据(在从通道读入缓冲区时)。
  • 位置(Position):指定了下一个将要被写入或者读取的元素索引,它的值由get()/put()方法自动更新,在新建立一个Buffer对象时,position被初始化为0。
  • 标记(Mark):下一个要被读或写的元素的索引。位置会自动由相应的 get( )和 put( )函数更新

get()方法从缓冲区中读取数据写入到输出通道,这会致使position的增长而limit保持不变,但position不会超过limit的值。函数

flip()方法 把limit设置为当前的position值 而且把position设置为 0性能

clear()方法将Buffer恢复到初始化状态this

public class BufferTest { public static void main(String[] args) throws IOException { ByteBufferTest(); } private static void ByteBufferTest(){ //分配新的byte缓冲区,参数为缓冲区容量 //新缓冲区的当前位置将为零,其界限(限制位置)将为其容量,它将具备一个底层实现数组,其数组偏移量将为零。
        ByteBuffer byteBuffer=ByteBuffer.allocate(10); output("初始化缓冲区:",byteBuffer); for(int i=0;i<byteBuffer.capacity()-1;i++){ byteBuffer.put(Byte.parseByte(new SecureRandom().nextInt(20)+"")); } output("写入缓冲区9个byte:",byteBuffer); byteBuffer.flip(); output("使用flip重置元素位置:",byteBuffer); while (byteBuffer.hasRemaining()){ System.out.print(byteBuffer.get()+"|"); } System.out.print("\n"); output("使用get读取元素:",byteBuffer); byteBuffer.clear(); output("恢复初始化态clear:",byteBuffer); } private static void output(String step, Buffer buffer) { System.out.println(step + " : "); System.out.print("capacity: " + buffer.capacity() + ", "); System.out.print("position: " + buffer.position() + ", "); System.out.println("limit: " + buffer.limit()); System.out.println("mark: " + buffer.mark()); System.out.println(); } } 初始化缓冲区: : 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]

 ByteBuffer.wrap( array ):将一个现有的数组,包装为缓冲区对象spa

 buffer.slice():建立子缓冲区,子缓冲区与原缓冲区是数据共享的

buffer.position( 3 ); buffer.limit( 7 ); ByteBuffer slice = buffer.slice();

 只读缓冲区:ByteBuffer readonly = buffer.asReadOnlyBuffer();

     只读缓冲区很是简单,能够读取它们,可是不能向它们写入数据。能够经过调用缓冲区的asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区,这个方法返回一个与原缓冲区彻底相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。若是原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化。

    若是尝试修改只读缓冲区的内容,则会报ReadOnlyBufferException异常。只读缓冲区对于保护数据颇有用。建立一个只读的缓冲区能够保证该缓冲区不会被修改。只能够把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。

 

2、直接缓冲区 DirectByteBuffer

   直接在堆外分配一个内存(即,native memory)来存储数据,程序经过JNI直接将数据读/写到堆外内存中。由于数据直接写入到了堆外内存中,因此这种方式就不会再在JVM管控的堆内再分配内存来存储数据了,也就不存在堆内内存和堆外内存数据拷贝的操做了。这样在进行I/O操做时,只须要将这个堆外内存地址传给JNI的I/O的函数就行了。底层的数据实际上是维护在操做系统的内存中,而不是jvm里,DirectByteBuffer里维护了一个引用address指向了数据,从而操做数据。实现zero copy(零拷贝)。

       间接内存HeapByteBuffer:对于HeapByteBuffer,数据的分配存储都在jvm堆上,当须要和io设备打交道的时候,会将jvm堆上所维护的byte[]拷贝至堆外内存,而后堆外内存直接和io设备交互。外设之因此要把jvm堆里的数据copy出来再操做,不是由于操做系统不能直接操做jvm内存,而是由于jvm在进行gc(垃圾回收)时,会对数据进行移动,一旦出现这种问题,外设就会出现数据错乱的状况。

直接缓冲区的建立:ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );

DirectByteBuffer的初始化:

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 { // 经过unsafe.allocateMemory分配堆外内存,并返回堆外内存的基地址
            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对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }

// Used only by direct buffers // NOTE: hoisted here for speed in JNI GetDirectBufferAddress //address就是堆外内存建立好后返回给JVM的地址,JVM内存须要维护的只是DirectByteBuffer对象,而具体数据的管理是由操做系统来管理的
    long address;

什么状况下使用堆外内存

  • 堆外内存适用于生命周期中等或较长的对象。( 若是是生命周期较短的对象,在YGC的时候就被回收了,就不存在大内存且生命周期较长的对象在FGC对应用形成的性能影响 )。
  • 直接的文件拷贝操做,或者I/O操做。直接使用堆外内存就能少去内存从用户内存拷贝到系统内存的操做,由于I/O操做是系统内核内存和设备间的通讯,而不是经过程序直接和外设通讯的。
  • 同时,还可使用 池+堆外内存 的组合方式,来对生命周期较短,但涉及到I/O操做的对象进行堆外内存的再使用。( Netty中就使用了该方式 )

两种方式的效率比较:

private static void directByteBufferTest()throws IOException{ long start=System.currentTimeMillis(); FileInputStream is=new FileInputStream("F:\\logs\\1g.rar"); FileOutputStream fos=new FileOutputStream("F:\\logs\\2g.rar"); 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)); } private static void heapByteBufferTest()throws IOException{ long start=System.currentTimeMillis(); FileInputStream is=new FileInputStream("F:\\logs\\1g.rar"); FileOutputStream fos=new FileOutputStream("F:\\logs\\3g.rar"); 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)); }

17行输出:DirectByteBuffer须要时间:30456

35行输出:HeapByteBuffer须要时间:45285

 3、内存映射文件I/O   MappedByteBuffer

 

 内存映射文件I/O是一种读和写文件数据的方法,它能够比常规的基于流或者基于通道的I/O快的多。内存映射文件I/O是经过使文件中的数据出现为 内存数组的内容来完成的,这其初听起来彷佛不过就是将整个文件读到内存中,可是事实上并非这样。通常来讲,只有文件中实际读取或者写入的部分才会映射到内存中。

FileChannel提供了map方法来把文件影射为内存映像文件: MappedByteBuffer map(int mode,long position,long size); 能够把文件的从position开始的size大小的区域映射为内存映像文件,映射内存缓冲区是个直接缓冲区,继承自ByteBuffer,但相对于ByteBuffer,它有更多的优势 读取快 写入快  随时随地写入;

mode指出了 可访问该内存映像文件的方式:            
    一、READ_ONLY,(只读): 试图修改获得的缓冲区将致使抛出 ReadOnlyBufferException.(MapMode.READ_ONLY)

    二、READ_WRITE(读/写): 对获得的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其余程序不必定是可见的。 (MapMode.READ_WRITE)
    三、PRIVATE(专用): 对获得的缓冲区的更改不会传播到文件,而且该更改对映射到同一文件的其余程序也不是可见的;相反,会建立缓冲区已修改部分的专用副本。 (MapMode.PRIVATE)

MappedByteBuffer 中的三个方法:

    a. fore();缓冲区是READ_WRITE模式下,此方法对缓冲区内容的修改强行写入文件
    b. load()将缓冲区的内容载入内存,并返回该缓冲区的引用
    c. isLoaded()若是缓冲区的内容在物理内存中,则返回真,不然返回假

使用MappedByteBuffer 将数据写入文件:

private static void mappedOutFile()throws IOException{ String str="I Love MappedByteBuffer"; RandomAccessFile raf = new RandomAccessFile( filePath, "rw" ); FileChannel fc = raf.getChannel(); byte [] msg=str.getBytes("UTF-8"); MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, msg.length); mbb.put(msg); fc.write(mbb); raf.close(); }
相关文章
相关标签/搜索