3.NIO直接缓冲区与非直接缓冲区

非直接缓冲区,缓冲区建立在JVM内存中,实际读写数据时,需要在OS和JVM之间进行数据拷贝,如下图:

非直接缓冲区

为什么不直接让磁盘控制器把数据送到用户空间的缓冲区呢?这样做有几个问题。首先,硬件通常不能直接访问用户空间。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责数据的分解、再组合工作,因此充当着中间人的角色

直接缓冲区,缓冲区建立在受操作系统管理的物理内存中,OS和JVM直接通过这块物理内存进行交互,没有了中间的拷贝环节,如下图:

直接缓冲区

所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件RAM)内存地址。这样做好处颇多,总结起来可分为两大类: 1. 一个以上的虚拟地址可指向同一个物理内存地址。 2. 虚拟内存空间可大于实际可用的硬件内存。
前一节提到,设备控制器不能通过 DMA 直接存储到用户空间,但通过利用上面提到的第一项,则可以达到相同效果。把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,这样,DMA 硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区(见图1-3)。

图 1-3. 内存空间多重映射

优点:速度更快,效率更高
缺点:①创建直接缓冲区将会有更多消耗②数据进入直接缓冲区后,后续写入磁盘等操作就完全由操作系统决定了,不受我们控制
什么时候用:缓冲区要长时间使用(数据本身需要长时间在内存
OR 缓冲区复用率很高),或者大数据量的操作(大文件才能体现出速度优势)。
如何使用

  1. 通过ByteBuffer.allocateDirect(),创建直接缓冲区
  2. 通过内存映射文件的方式
  3. 使用通道直接传输

下面以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操作,能以更加高效的方式进行文件的读取操作

你可能感兴趣的:(3.NIO直接缓冲区与非直接缓冲区)