文件拷贝:先学会踩坑
我见过太多人写的文件拷贝代码,第一次都是错的。
不是逻辑错误,是性能灾难——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 亿次?
