Skip to content

文件拷贝:先学会踩坑

我见过太多人写的文件拷贝代码,第一次都是错的。

不是逻辑错误,是性能灾难——1GB 文件跑了 10 分钟,CPU 跑满,磁盘 IO 飙升,程序卡死。

问题不在于代码「不对」,而在于代码「太慢」。让我们从最原始的写法开始,一步步优化到生产级。

最差的写法

java
public static void copyFileNoBuffer(String src, String dst) throws IOException {
    try (
        FileInputStream in = new FileInputStream(src);
        FileOutputStream out = new FileOutputStream(dst)
    ) {
        int b;
        while ((b = in.read()) != -1) {  // 每次读 1 字节
            out.write(b);                  // 每次写 1 字节
        }
    }
}

这段代码逻辑完全正确,1GB 文件拷贝出来数据一模一样。

但代价是:每读写 1 字节就触发一次系统调用,1GB 文件 = 200 万次系统调用

一次系统调用的开销,相当于几千次内存操作。这不是慢一点,是慢几个数量级。

大多数人的第一次优化

java
public static void copyFileWithArray(String src, String dst) throws IOException {
    try (
        FileInputStream in = new FileInputStream(src);
        FileOutputStream out = new FileOutputStream(dst)
    ) {
        byte[] buffer = new byte[8192];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
    }
}

好多了!1GB 文件只需要约 128,000 次系统调用。

但还不够。问题在于 FileInputStream.read(byte[]) 每次调用都会触发系统调用。

正确的写法:加缓冲流

java
public static void copyFile(String src, String dst) throws IOException {
    try (
        BufferedInputStream in = new BufferedInputStream(
            new FileInputStream(src));
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(dst))
    ) {
        byte[] buffer = new byte[8192];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
    }
}

加了缓冲层后,1GB 文件只需要约 256 次系统调用

性能对比

方式1GB 文件系统调用次数预估耗时
逐字节(无缓冲)~2 亿次数分钟
批量 8KB(无缓冲)~128,000 次~30 秒
批量 8KB(有缓冲)~256 次<1 秒

核心原理:系统调用的开销远大于内存读写。每次 read/write 的数据越多,系统调用次数越少,速度越快。

带进度显示的版本

拷贝大文件时,用户需要知道进度:

java
public static void copyFileWithProgress(String src, String dst) throws IOException {
    File srcFile = new File(src);
    long totalSize = srcFile.length();
    long copied = 0;

    try (
        BufferedInputStream in = new BufferedInputStream(
            new FileInputStream(src));
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(dst))
    ) {
        byte[] buffer = new byte[8192];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
            copied += len;
            int percent = (int) (copied * 100 / totalSize);
            System.out.printf("\r拷贝进度: %d%%", percent);
        }
        System.out.println();
    }
}

大文件:增大缓冲区

对于 GB 级别的超大文件,用更大的缓冲区减少系统调用:

java
public static void copyLargeFile(String src, String dst, int bufferSize)
        throws IOException {
    try (
        BufferedInputStream in = new BufferedInputStream(
            new FileInputStream(src), bufferSize);
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(dst), bufferSize)
    ) {
        byte[] buffer = new byte[bufferSize];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
    }
}

// 使用:8MB 缓冲区,适合拷贝大视频文件
copyLargeFile("video.mp4", "video_copy.mp4", 8 * 1024 * 1024);

追求极致:零拷贝

如果这还不够快,Java NIO 提供了 FileChannel.transferTo(),使用操作系统的 sendfile 系统调用,绕过用户态内存拷贝:

java
public static void copyFileZeroCopy(String src, String dst) throws IOException {
    try (
        FileChannel in = new FileInputStream(src).getChannel();
        FileChannel out = new FileOutputStream(dst).getChannel()
    ) {
        long size = in.size();
        long transferred = 0;
        while (transferred < size) {
            transferred += in.transferTo(
                transferred,
                size - transferred,
                out
            );
        }
    }
}

选型建议

场景推荐方案
普通文件(MB 级)BufferedInputStream + BufferedOutputStream
超大文件(GB 级)BufferedInputStream + 大缓冲区(1MB+)
追求极致性能FileChannel.transferTo() 零拷贝
需要进度条手动计算百分比,循环拷贝

记住:文件拷贝的核心不是「会不会写」,而是「写多少次」。Buffer 用得好,256 次系统调用就能搞定的事,何必用 2 亿次?

基于 VitePress 构建