12 java的文件拷贝方式及NIO相关知识扩展

java的文件拷贝方式及NIO相关知识扩展:

java的文件拷贝方式:

  1. 利于FileInputStream读取,利用FileOutputStream写入;代码如下:(此处buffer与Buffered stream效果一致,取其一即可,无需两者同时使用,建议使用buffered stream)

       public static void copy(String source,String target) throws IOException {
         try(FileInputStream input = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(target)){
           BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
           BufferedInputStream bufferedInput = new BufferedInputStream(input);
           byte[] buffer = new byte[10240];
           int size = 0;
           while((size = bufferedInput.read(buffer)) > 0) {
             bufferedOut.write(buffer,0,size);
           }
           bufferedOut.flush();
         }
       }
    
  2. 利用java.nio提供的trasferTo或者trasferFrom方法实现,它更能利用现代操作系统的底层机制,避免不必要的拷贝和上下文切换,代码如下:

       public static void copy2(String source,String target) throws IOException {
         try(FileChannel input = new FileInputStream(source).getChannel();
             FileChannel out = new FileOutputStream(target).getChannel();){
           long length = input.size();
           //while( length > 0) {
           //length -= input.transferTo(input.position(),length,out);
           //}
           //or
           while(length > 0) {
             //transferFrom并不会改变out流的position位置,
             //但是会改变的input流的position位置;
             length -= out.transferFrom(input,input.position(),length);
           }
         }
       }
    

为什么transferTo或者transferFrom有性能优势:

  1. 理解内核态空间和用户态空间:

    1. 普通应用程序运行于用户态空间,权限受限,无法对底层IO进行操作,相关的操作需要切换到内核态空间才能执行;这就会带来空间切换的开销;系统内核、硬件驱动都运行于内核态空间,具有较高的权限;
  1. 用户态空间下进行文件拷贝示意图:
用户态空间文件拷贝示意图.png
  1. 用户态空间下文件拷贝实际上进行了多次上下文切换,且数据需要先从磁盘读取到内核缓存,在切换到用户态将数据从内核缓存读取到用户缓存;写入操作也是类似,这就带来了额外开销,降低IO效率;
  1. transferTo或者transferFrom则能利用底层操作系统机制,在Linux上使用零拷贝技术,数据传输无需用户态参与,省去了上下文切换的开销与不必要的内存拷贝;示意图如下:
    transferTo文件拷贝示意图.png

标准库(JDK1.8u201)提供的相关拷贝(java.nio.file.Files.copy):

  1. public static Path copy(Path source, Path target, CopyOption... options)throws IOException

  2. public static long copy(InputStream in, Path target, CopyOption... options) throws IOException

  3. public static long copy(Path source, OutputStream out) throws IOException {

以上方法均使用的是用户态空间拷贝;其中第一个方法是使用本地技术实现的用户态拷贝;1.8以后的JDK版本下面两个函数版本可能会采用transferTo或者transferFrom进行优化;

提高文件拷贝性能的原则:

  1. 尽量使用缓存,合理减少IO访问次数;
  2. 使用transferTo机制,减少上下文切换和不必要内存拷贝;
  3. 减少不必要的转换,直接传输二进制;如:编解码、序列化和反序列化;

掌握NIO Buffer;

1. buffer属性:

  1. capacity,它反映这个buffer的大小;
  2. limit:它反映这个buffer的可读或者可写限度;
  3. position:该buffer目前读取、写入的位置;都是从0开始的;
  4. mark:便利性的考虑,可以通过mark()记录地址,reset将position设置为上一次mark的位置;clear,flip,rewind都会清零mark(设置为-1);

2. buffer注意:

  1. 即使调用了flip方法,也需要注意的是该buffer依然能够写入数据,并且position会随着写入字节水涨船高,如果超过limit大小,则会抛出BufferOverflowException;
  2. buffer.get()方法可以传入一个byte数组,注意,该方法会根据byte数组的大小来决定读取多少,使用get方法从0开始填充到数组的最后一位,如果byte大小超过了ByteBuffer的可以读取的限度,则会抛出BufferUnderflowException;

3. buffer的方法:

  1. put(),get():基本的写入和读取方法,会影响position的数值,还有putInt,putFloat等版本,int占4个byte空间,如果是ByteBuffer则position会增长4;
  2. flip方法:将position设置为0,limit设置为当前的position那里,并清除mark;
  3. rewind:position设置为0,请出mark;
  4. clear:position设置为0,limit设置为capacity,清除mark;
  5. hasRemaining():返回当前position和limit之间是否还有元素;
  6. Remaining():返回还有多少元素;
  7. reset():position设置为mark的位置;

4. direct buffer、MappedByteBuffer和垃圾收集

  1. MappedByteBuffer:FileChannel.map()可以创建,本质上也是一种DirectBuffer,它将文件按照指定大小直接映射为内存区域,当程序访问内存区域时,将直接操作文件数据,省去了数据从内核空间向用户空间传输的损耗,使用写模式时注意使用RandomAccessFile与其配合,因为无论是FileInputStream还是FileOutputStream均不能满足其map中的读写模式,但又没有提供只写模式,但其也存在一些问题:如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的;

  2. 获取direct buffer:ByteBuffer.allocateDirect(1000)来获取DirectBuffer;

  3. 检测是否是DirectBuffer:buffer提供了isDirect函数来检测是否是DirectBuffer

  4. Direct Buffer 在实际使用过程中,java会尽量对其仅作本地IO操作,对于很多大数据量的IO操作,会带来非常大的性能优势:

    1. 因为生命周期内地址都不会再改变,内核可以安全对其进行访问,所以很多IO操作都会很高效;
    2. 因为是堆外对象,省去了额外的维护开销,访问效率更高;
  5. Direct Buffer使用注意:

    1. 创建和销毁都比一般buffer开销要大;

    2. -Xmx不能影响其使用,因为其是堆外存储;限制方式如下:
      -XX:MaxDirectMemorySize=512M

      所以在使用DirectBuffer时,应注意内存大小需要考虑其空间占用;否则容易导致OOM;

    3. 大部分gc不会主动收集DirectBuffer;它的销毁往往要等到FullGC;

  6. 使用建议:

    1. 重复使用DirectBuffer;
    2. 跟踪和诊断DirectBuffer内存占用:
      1. -XX:NativeMemoryTracking={summary|detail}可以开启Native Memory Tracking(NMT)特性,激活 NMT 通常都会导致 JVM 出现 5%~10% 的性能下降,采用以下命令进行交互式对比:

         // 打印NMT信息
         jcmd  VM.native_memory detail 
         
         // 进行baseline,以对比分配内存变化
         jcmd  VM.native_memory baseline
         
         // 进行baseline,以对比分配内存变化
         jcmd  VM.native_memory detail.diff
        

Scattering Reads And Gathering Writes

定义:

Scattering Reads

Scattering Reads是指数据从一个channel读取到多个buffer中。如下图描述:


scatteringRead.png

Gathering Writes

Gathering Writes是指数据从多个buffer写入到同一个channel。如下图描述:


gatherWrite.png

使用语法:

ByteBuffer header = ByteBuffer.allocate(128);  
ByteBuffer body = ByteBuffer.allocate(1024);  
ByteBuffer[] dataBuffers = {header, body};
//注意只会将buffer中的有效数据写入或者读取channel,也就是说受limit的影响,
//如果channel中的数据大于buffer总空间,不会抛出异常,依次读取写入buffer,能读多少读多少;
channle.write(dataBuffers);
channel.read(resultBuffers);  

你可能感兴趣的:(12 java的文件拷贝方式及NIO相关知识扩展)