常见Java文件拷贝方式及效率

#方法一

  • 利用Java.IO,设置缓冲区,通过字节输入流从源文件中将数据读入缓冲区,然后再用字节输出流输出到目标文件中。

public static void copyFileByChannel(File source, File dest) throws IOException {
    try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
         FileChannel targetChannel = new FileOutputStream(dest).getChannel();){
        for (long count = sourceChannel.size() ;count>0 ;) {
            long transferred = sourceChannel.transferTo(
                    sourceChannel.position(), count, targetChannel);         
					sourceChannel.position(sourceChannel.position() + transferred);
            count -= transferred;
        }
    }
 }

方法二

  • 利用Java.NIO 类库提供的tranferTo(sourceChannel将源文件数据传输到targetChannel,在输出到目标文件),transferFrom(targetChannel从sourceChannel将源文件数据读取到放入目标文件)方法实现

public static void copyFileByChannel(File source, File dest) throws
        IOException {
    try (FileChannel sourceChannel = new FileInputStream(source)
            .getChannel();
         FileChannel targetChannel = new FileOutputStream(dest).getChannel
                 ();){
        for (long count = sourceChannel.size() ;count>0 ;) {
            long transferred = sourceChannel.transferTo(
                    sourceChannel.position(), count, targetChannel);            sourceChannel.position(sourceChannel.position() + transferred);
            count -= transferred;
        }
    }
 }

#方法三

Java标准类库本身提高的几种Files.copy实现

二者差异

对于复制效率,也跟OS和配置相关。
总体来看,NIO的transferTo,transferFrom方式更快,因其利用现代OS底层机制,避免不必要拷贝和上下文切换

知识扩展

拷贝实现机制分析

Java.IO相关概念及流程

操作系统基本概念:
(1).用户态空间(UserSpace):提供普通应用和服务使用
(2).内核态空间(Kernel Space):操作系统内核、硬件驱动等运行在内核态空间,具有较高特权
流程:
当使用输入流、输出流进行读写时,实际进行多次上下文切换,如应用将读取数据时,执行如下流程
读操作:先将数据从磁盘复制到内核态空间,在从内核态空间复制用户态空间缓存
写操作:将数据从用户态空间缓存区域复制到内核态空间,再从内核态空间复制到磁盘,完成写入。
缺点:多次数据复制,带来一定额外开销,可能会降低IO效率

  • 复制流程
    Alt java io 文件复制流程

Java.NIO流程

  • NIO流程
基于NIO transferTo实现方式,在Linux和Unix上,则会使用零拷贝技术,应用读取数据时
读操作:将数据从磁盘复制到内核态空间,完成读取
写操作:将数据从内核态空间复制到磁盘,完成写入。
优点:数据传输(网路IO)、文件复制不需要用户态空间参与,省去上下文切换开销和不必要拷贝,提高应用拷贝性能
tansferTo不仅应用文件拷贝,而且使用网路IO,与其类似,读取磁盘文件,然后进行Socket发生,同样享受该机制带来的性能和扩展性提高。
  • 流程图示
    Java Nio transferFrom/transferTo 实现

Java IO/NIO 源码结构

Java.IO标准库提供的为文件复制方法

  • java.nio.file.Files.copy源码分析
 public static long copy(Path source, OutputStream out) throws IOException {
        // ensure not null before opening file
        Objects.requireNonNull(out);

        try (InputStream in = newInputStream(source)) {
            return copy(in, out);
        }
    }

public static Path copy(Path source, Path target, CopyOption... options)
    throws IOException
	
	
public static long copy(InputStream in, Path target, CopyOption... options)
        throws IOException
    {
        // ensure not null before opening file
        Objects.requireNonNull(in);

        // check for REPLACE_EXISTING
        boolean replaceExisting = false;
        for (CopyOption opt: options) {
            if (opt == StandardCopyOption.REPLACE_EXISTING) {
                replaceExisting = true;
            } else {
                if (opt == null) {
                    throw new NullPointerException("options contains 'null'");
                }  else {
                    throw new UnsupportedOperationException(opt + " not supported");
                }
            }
        }

        // attempt to delete an existing file
        SecurityException se = null;
        if (replaceExisting) {
            try {
                deleteIfExists(target);
            } catch (SecurityException x) {
                se = x;
            }
        }

        // attempt to create target file. If it fails with
        // FileAlreadyExistsException then it may be because the security
        // manager prevented us from deleting the file, in which case we just
        // throw the SecurityException.
        OutputStream ostream;
        try {
            ostream = newOutputStream(target, StandardOpenOption.CREATE_NEW,
                                              StandardOpenOption.WRITE);
        } catch (FileAlreadyExistsException x) {
            if (se != null)
                throw se;
            // someone else won the race and created the file
            throw x;
        }

        // do the copy
        try (OutputStream out = ostream) {
            return copy(in, out);
        }
    }

	

public static Path copy(Path source, Path target, CopyOption... options)
        throws IOException
    {
        FileSystemProvider provider = provider(source);
        if (provider(target) == provider) {
            // same provider
            provider.copy(source, target, options);
        } else {
            // different providers
            CopyMoveHelper.copyToForeignTarget(source, target, options);
        }
        return target;
    }

可以看到copy不仅仅支持文件操作,也支持从流中读取文件落盘,从磁盘中读取到流落盘
后面两种方法,可以在方法实现中看到用的是InputStream.transferTo

(2)jdk源码中,内部实现和公共API定义不可以简单关联上,NIO部分代码甚至是定义为模板而不是Java源文件,在build过程自动生成源码
接下来介绍部分jdk代码机制和如何绕过因此障碍
1.首先,直接跟踪,发现FileSystemProvider只是个抽象类,阅读它的源码能够理解到,原来文件系统实际逻辑存在jdk中,公共API其实是通过
ServiceLoader机制加载一系列文件系统实现,然后提供服务
2.可在jdk源码搜索FileSystemProvider 和NIO,可以定位到sun/nio/fs.我们知道NIO底层和操作系统紧密相关,每个平台都有自己的部分持有文件系统逻辑
  • jdk内部实现文件系统实际逻辑的图示
     jdkt  ransferTo  内部实现

  • 省略细节,一步步定位到UnixFileSystemProvider->UnixCopyFile.Transfer,发现其是本地方法

  • 最后明确定位到UnixCopyFile.c,其内部清除说明只是简单用户态空间拷贝

  • 因此,我们明确这个场景copy方法不是利用transferTo,而是本地技术实现的用户态拷贝

提高IO拷贝性能的原则

  • 程序中,使用换成等机制,合理减少IO次数(在网络通信中,TCP传输,window大小可以看做是类似思路)

  • 使用transferTo机制,减少上下文切换和额外IO操作

  • 尽量减少不必要转换过程:编解码,对象序列化和反序列化,操作文本文件或网络通信,如果过程中不需文本信息,可以考虑将其作二进制信息传输

掌握NIO Buffer

  • NIO提供出Boolean之外基本数据类型缓存
    在这里插入图片描述

  • buffer基本概念

1.capacity: 反映buffer大小,即数组长度
2.position :要操作的数据起始位置,如读取数据从哪个位置读取
3.limit:相当于操作的限额。在读如或写入意义不同。读取操作时,limit设置到所容纳数据上限(实际数据);而在写入时,则设置容量或容量以下可写限度
4.mark:记录上一次position的位置,默认0,是便利性考虑,可选。
  • buffer基本操作
1.创建ByteBuffer,放入数据,capacity是缓冲区大小,而position位置是0,limit是capacity的大小
2.当写入几个字节数据,position会相应增加,但不会大于limit(buffer限额)
3.如果去除前面写入数据,调用flip方法,转换为从ByteBuffer中读取数据,position设置为0,limit设置为以前的position位置(数据的实际长度所在位置)
4.如果想从头读一本,使用rewind,让limit不变,position再次设置为0
  • Direct Buffer和垃圾收集
1.Direct Buffer:根据buffer定义,会发现isDirect()方法,返回当前Buffer是否为Direct类型,
因为Java提供堆外、堆内Buffer,也可以使用allocate或allocateDirect方法直接创建
2.MappedByteBuffer:将文件按照指定大小直接映射为内存区域,当程序访问这个内存区域时直接操作这块文件数据,省去将数据从内核向用户空间传输的损耗,
可以使用FileChannel.map进行创建,本质也是Direct Buffer
  • 最佳实践
Java 尽量对Direct Buffer作本地IO操作,对于很多大数据量的IO密集操作,带来非常大的性能优势,优势如下:
1.Direct Buffer生命周期内地址不会发生更改,进而内核可以安全地对其访问,故IO操作会更高效
2.减少堆内对象存储可能额外维护工作,故访问效率有所提高
缺点:Direct Buffer创建、销毁比一般堆内Buffer增加部分开销,建议用于长期使用、数据较大场景。
  • Direct Buffer 参数设置
1.Direct Buffer不在堆上,所以Xms,Xmx之类参数不影响Direct Buffer等堆外成员所使用的内存额度,使用如下设置
-XX:MaxDirectMemorySize=512M

2.从参数设置和内存问题排查来看,计算java可使用内存大小时,不仅考虑堆内需要,还要考虑Direct Buffer等一系列对外因素
如果出现内存不足,使用堆外内存也是一种可能性

3.Direct Buffer不会被GC收集,需手动进行回收
应用显示调用System.GC()除非Direct Buffer回收或使用Direct Buffer框架,让框架进行释放(如PlatformDepentdent0)

4.重复使用Direct Buffer,保证效率,减少创建、销毁频次
  • 跟踪和诊断Direct Buffer内存占用
1.GC日志不包含Direct Buffer等信息,jdk8后有NMT(Native Memory Tracking)特性进行诊断,开启设置:
-XX:NativeMemoryTracking={summary|detail}

2.tradeOff:集合NMT会导致JVM出现5%-10%的性能下降,慎重考虑

3.使用下述目录进行交互比较:

// 打印NMT信息
jcmd <pid> VM.native_memory detail 

// 进行baseline,以对比分配内存变化
jcmd <pid> VM.native_memory baseline

// 进行baseline,以对比分配内存变化
jcmd <pid> VM.native_memory detail.diff


4.可在Internal部分发现Direct Buffer内存使用信息,其底层利用unsafe_allocatememory,严格说,这不JVM内部使用内存,不归JVM管理
如jdk11 后,其实归类在other部分里面,jdk9输出片段如下,"+"表示diff命令发现的分配变化

-Internal (reserved=679KB +4KB, committed=679KB +4KB)
              (malloc=615KB +4KB #1571 +4)
              (mmap: reserved=64KB, committed=64KB)

发布了150 篇原创文章 · 获赞 15 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/dymkkj/article/details/104508311
今日推荐