最近项目里有个需求需要实现文件拷贝, 在 java 中文件拷贝流的读写, 很容易就想到 IO 中的 InputStream 和 OutputStream 之类的, 但是上网查了一下文件拷贝也是有很多种方法的, 除了 IO, 还有 NIO,Apache 提供的工具类, JDK 自带的文件拷贝方法
IO 拷贝
- public class IOFileCopy {
- private static final int BUFFER_SIZE = 1024;
- public static void copyFile(String source, String target) {
- long start = System.currentTimeMillis();
- try(InputStream in = new FileInputStream(new File(source));
- OutputStream out = new FileOutputStream(new File(target))) {
- byte[] buffer = new byte[BUFFER_SIZE];
- int len;
- while ((len = in.read(buffer))> 0) {
- out.write(buffer, 0, len);
- }
- System.out.println(String.format("IO file copy cost %d msc", System.currentTimeMillis() - start));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
传统 IO 中文件读取过程可以分为以下几步:
内核从磁盘读取数据到缓冲区, 这个过程由磁盘操作器通过 DMA 操作将数据从磁盘读取到内核缓冲区, 该过程不依赖 CPU
用户进程在将数据从内核缓冲区拷贝到用户空间缓冲区
用户进程从用户空间缓冲区读取数据
NIO 拷贝
NIO 进行文件拷贝有两种实现方式, 一是通过管道, 而是通过文件内存内存映射
public class NIOFileCopy { public static void copyFile(String source, String target) { long start = System.currentTimeMillis(); try(FileChannel input = new FileInputStream(new File(source)).getChannel(); FileChannel output = new FileOutputStream(new File(target)).getChannel()) { output.transferFrom(input, 0, input.size()); } catch (Exception e) { e.printStackTrace(); } System.out.println(String.format("NIO file copy cost %d msc", System.currentTimeMillis() - start)); } }
文件内存映射:
把内核空间地址与用户空间的虚拟地址映射到同一个物理地址, DMA 硬件可以填充对内核与用户空间进程同时可见的缓冲区了. 用户进程直接从内存中读取文件内容, 应用只需要和内存打交道, 不需要进行缓冲区来回拷贝, 大大提高了 IO 拷贝的效率. 加载内存映射文件所使用的内存在 Java 堆区之外
public class NIOFileCopy2 { public static void copyFile(String source, String target) { long start = System.currentTimeMillis(); try(FileInputStream fis = new FileInputStream(new File(source)); FileOutputStream fos = new FileOutputStream(new File(target))) { FileChannel sourceChannel = fis.getChannel(); FileChannel targetChannel = fos.getChannel(); MappedByteBuffer mappedByteBuffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, sourceChannel.size()); targetChannel.write(mappedByteBuffer); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } System.out.println(String.format("NIO memory reflect file copy cost %d msc", System.currentTimeMillis() - start)); File targetFile = new File(target); targetFile.delete(); } }
NIO 内存映射文件拷贝可以分为以下几步
NIO 的内存映射实际上就是少了一次从内核空间拷贝到用户空间的过程, 将对用户缓冲区的读改为从内存读取
Files#copyFile 方法 public class FilesCopy { public static void copyFile(String source, String target) { long start = System.currentTimeMillis(); try { File sourceFile = new File(source); File targetFile = new File(target); Files.copy(sourceFile.toPath(), targetFile.toPath()); } catch (IOException e) { e.printStackTrace(); } System.out.println(String.format("FileCopy file copy cost %d msc", System.currentTimeMillis() - start)); } } FileUtils#copyFile 方法
使用 FileUtils 之前需先引入依赖
依赖
<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> FileUtils#copyFile 封装类: FileUtilsCopy.java public class FileUtilsCopy { public static void copyFile(String source, String target) { long start = System.currentTimeMillis(); try { FileUtils.copyFile(new File(source), new File(target)); } catch (IOException e) { e.printStackTrace(); } System.out.println(String.format("FileUtils file copy cost %d msc", System.currentTimeMillis() - start)); } }
性能比较
既然有这么多种实现方法, 肯定要从中选择性能最佳的
测试环境:
Windows 10
CPU 6 核
JDK1.8
测试代码: PerformTest.java
public class PerformTest { private static final String source1 = "input/test1.txt"; private static final String source2 = "input/test2.txt"; private static final String source3 = "input/test3.txt"; private static final String source4 = "input/test4.txt"; private static final String target1 = "output/test1.txt"; private static final String target2 = "output/test2.txt"; private static final String target3 = "output/test3.txt"; private static final String target4 = "output/test4.txt"; public static void main(String[] args) { IOFileCopy.copyFile(source1, target1); NIOFileCopy.copyFile(source2, target2); FilesCopy.copyFile(source3, target3); FileUtilsCopy.copyFile(source4, target4); } }
总共执行了五次, 读写的文件大小分别为 9KB,23KB,239KB,1.77MB,12.7MB
注意: 单位均为毫秒
从执行结果来看:
文件很小时 => IO> NIO[内存映射]> NIO[管道]> Files#copy> FileUtils#copyFile
在文件较小时 => NIO[内存映射]> IO> NIO[管道]> Files#copy> FileUtils#copyFile
在文件较大时 => NIO[内存映射]>> NIO[管道]> IO> Files#copy> FileUtils#copyFile
修改 IO 缓冲区大小对拷贝效率有影响, 但是并不是越大性能越好, 稍大于拷贝文件大小即可
文件较小时, IO 效率高于 NIO,NIO 底层实现较为复杂, NIO 的优势不明显. 同时 NIO 内存映射初始化耗时, 所以在文件较小时和 IO 复制相比没有优势
如果追求效率可以选择 NIO 的内存映射去实现文件拷贝, 但是对于大文件使用内存映射拷贝要格外关注系统内存的使用率. 推荐: 大文件拷贝使用内存映射, 原文是这样的:
For most operating systems, mapping a file into memory is more expensive than reading or writing a few tens of kilobytes of data via the usual { @link #read read } and { @link #write write } methods. From the standpoint of performance it is generally only worth mapping relatively large files into memory
绝大多数操作系统的内存映射开销大于 IO 开销
同时通过测试结果来看, 工具类和 JDK 提供的文件复制方法效果并不高, 如果不追求效率还是可以使用一下, 毕竟能少写一行代码就少写一行代码, 写代码没有摸鱼来的快乐
来源: http://www.bubuko.com/infodetail-3415177.html