美文网首页
Java 文件拷贝的方式

Java 文件拷贝的方式

作者: wean_a23e | 来源:发表于2018-12-14 17:06 被阅读18次

    三种方式文件拷贝的方式

    1. 通过阻塞流实现
    public static void copyFileByStream(File source, File dest) throws
            IOException {
        try (InputStream is = new FileInputStream(source);
             OutputStream os = new FileOutputStream(dest);){
            byte[] buffer = new byte[1024];
            int length;
            while ((length = is.read(buffer)) > 0) {
                os.write(buffer, 0, length);
            }
        }
     }
    

    优点是实现简单,而且在实际使用中,简单的场景下可能是最快的。

    1. 通过 transferTo/From 实现
    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;
            }
        }
     }
    

    缺点是写起来比 stream 复杂。优点是利用直接在内核态和操作,避免了在用户态传输数据的消耗。理论上是最快的拷贝方式。

    1. 使用 Files.copy()

    优点是使用最为简洁,而且不只是文件流的拷贝。

    拷贝实现机制分析

    前面提到的三种拷贝方式,实现流程都是一样的:从一个地方,复制一段数据到内存,再从内存中把这段数据输出到另一个地方

    唯一的细节不同处,就是数据在这个过程中需不需要经过用户态空间。

    1. 当我们使用输入输出流时,实际上是进行了多次上下文切换,比如应用读取数据时,现在内核态将数据从磁盘读取到内核缓存,再切换到用户态,将数据从内核缓存读取到用户缓存。流程图如下:

    显然这种方式需要额外的开销,会降低 IO 效率。

    1. 当我们使用 NIO transferTo 时,在 Linux 和 Unix 系统上,则会使用到零拷贝技术,即数据传输不需要经过用户态,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。而且,transferTo 还可以应用在 Socket 传输中,同样可以享受这种机制带来的性能和扩展性提高。

    Files.copy() 源码分析

    前面提到,Java 标准库直接给我们提供了文件拷贝的 API。他有三个重载版本:

    从参数可以看出,这个方法不仅仅是只支持文件之间的操作,还可以在各种流中传输文件。

    后两种实现方式,从底层源码可以看到,是直接利用阻塞 IO stream 配合一个 byte[] 数组作为缓冲区实现文件拷贝的。

        private static long copy(InputStream source, OutputStream sink)
            throws IOException
        {
            long nread = 0L;
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = source.read(buf)) > 0) {
                sink.write(buf, 0, n);
                nread += n;
            }
            return nread;
        }
    

    而第一种拷贝方式,则会先具体区分文件系统再进行处理:

    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;
    }
    

    追踪同类型文件系统中的拷贝,发现内部实现和公共 API 之间不是直接关联的,NIO 部分甚至是定义为模板而不是 Java 源文件,在 build 过程中生成源码,下面介绍下部分 JDK 代码机制和如何绕过隐藏障碍。

    • 首先,直接跟踪 FileSystemProvider,发现这是一个抽象类,根据注释可以直接理解到,文件系统的实际逻辑存在于 JDK 的内部实现中,公共 API 其实是通过 ServiceLoader 机制加载一系列文件系统实现,然后提供服务。
    • 在 JDK 源码中搜索 FileSystemProvider 的具体实现,可以定位到 sun/nio/fs,这里存放着具体平台的部分特有文件系统逻辑。
    • 对于 Linux 下,省略掉一些细节,最后一步一步定位到 UnixFileSystemProvider -> UnixCopyFile.Transfer,可以看到这是一个本地方法。
    • 最终明确定位到 UnixCopyFile.c,其内部实现清楚说明这只是简单的用户态空间拷贝。

    总结下来,可以知道,这个 JDK 提供的接口,其实只是简单的本地技术实现的用户态拷贝。

    如何提高类似拷贝 IO 的性能

    1. 利用缓冲区,减少 IO 次数
    2. 使用 transferTo/From 机制,减少上下文切换和额外的 IO 操作。
    3. 减少不必要的转换过程。比如编解码、对象序列化和反序列化,比如操作文本文件或者网络通信,如果不是过程中需要使用到文本信息,可以考虑直接传输二进制信息而不用将二进制信息转换成字符串。

    Direct Buffer 和垃圾收集

    这里重点介绍两种特别的 buffer。

    • DirectBuffer : 在 Buffer 的方法定义中,有一个 isDirect() 方法,返回当前方法是否是 Direct 类型。这是 Java 提供的堆外 Buffer。可以使用 allocateDirect 方法直接创建。
    • MappedByteBuffer : 它将文件按照指定大小直接映射为内存区域,当程序访问这个内存区域时,将直接操作这块文件数据,省去了将数据从内核空间向用户空间传输的损耗。我们可以使用 FileChannel.map 创建 MappedByteBuffer,它本质上也是种 Direct Buffer。

    在实际使用中,Java 会尽量对 Direct Buffer 仅作本地 IO 操作,对于很大数据量的 IO 密集型操作,可能会带来很大的性能优势,因为:

    • Direct Buffer 在生命周期内内存地址都不会再做改变,进而内核可以直接安全地对其访问,很多 IO 操作会很高效。
    • Direct Buffer 避免了堆内对象需要的额外的维护工作,提高了效率。

    但是,高效背后也是高成本。Direct Buffer 在创建和销毁过程中,都会比一般的 Buffer 增加部分开销,所以通常应该用于长期使用、数据量较大的场景。

    Direct Buffer 因为不在堆上,所以 Xmx 参数对它无效,可以使用下面的代码设置堆外内存的大小:

    -XX:MaxDirectMemorySize=512M
    

    从参数设置和内存问题排查来看,我们在设置 JVM 需要的内存时,如果用到了堆外内存,还应考虑堆外内存的开销。而出现了 OOM 问题时,也应该考虑是否是堆外内存不够的可能性。

    对于 Direct Buffer 的回收,可以考虑:

    • 在应用程序中,显式调用 System.gc() 来强制触发。
    • 另一种思路是,在大量使用 Direct Buffer 的部分框架中,框架会自己在程序中调用释放方法,Netty 就是这么做的。
    • 重复使用 Direct Buffer,而不是每次需要再创建,用完立刻销毁。

    跟踪诊断 Direct Buffer 的内存占用的方法

    在普通的垃圾收集日志中,并不包含 Direct Buffer 等信息,所以 Direct Buffer 的内存诊断是个比较头疼的问题。在 java 8 以后,我们可以使用 Native Memory Tracking (NMT) 来诊断,在启动程序时加上下面的参数可以激活 NMT,但是会导致 JVM 出现 5%~10% 的性能下降:

    -XX:NativeMemoryTracking={summary|detail}
    

    开启 NMT 后,就可以通过下面的命令进行交互式对比:

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

    相关文章

      网友评论

          本文标题:Java 文件拷贝的方式

          本文链接:https://www.haomeiwen.com/subject/jphghqtx.html