大文件处理优化
GB 级大文件不能一次性读入内存。
有人问过我:为什么 Files.readAllBytes() 读一个 10GB 的文件会 OOM?
答案是:它试图把 10GB 全部加载到内存。
这一节讲清楚如何高效处理大文件。
核心原则:分块处理
java
// ❌ 错误:一次性读入内存
byte[] data = Files.readAllBytes(Path.of("bigfile.dat")); // 内存爆炸
// ✅ 正确:分块读取
try (
BufferedInputStream in = new BufferedInputStream(
new FileInputStream("bigfile.dat"), 8 * 1024 * 1024) // 8MB 缓冲
) {
byte[] buffer = new byte[8 * 1024 * 1024]; // 8MB 缓冲区
int len;
while ((len = in.read(buffer)) != -1) {
process(buffer, 0, len); // 处理当前块
}
}内存估算
| 文件大小 | 推荐缓冲区 | 峰值内存占用 |
|---|---|---|
| < 100MB | 8KB | < 100MB |
| 100MB ~ 1GB | 1MB | < 200MB |
| 1GB ~ 10GB | 8MB | < 500MB |
| > 10GB | 内存映射(分段) | 可控 |
方案一:分块读取
适合:需要处理文件内容的每一部分。
java
public static void processLargeFile(String path) throws IOException {
try (
BufferedInputStream in = new BufferedInputStream(
new FileInputStream(path), 8 * 1024 * 1024)
) {
byte[] buffer = new byte[8 * 1024 * 1024]; // 8MB
int len;
while ((len = in.read(buffer)) != -1) {
process(buffer, 0, len);
}
}
}
private static void process(byte[] buffer, int offset, int len) {
// 处理数据块
}方案二:NIO 内存映射
适合:需要频繁随机访问的大文件。
java
public static void processLargeFile(String path) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(path, "rw");
FileChannel channel = raf.getChannel()) {
long fileSize = channel.size();
long position = 0;
long chunkSize = 1024L * 1024 * 1024; // 1GB 每段
while (position < fileSize) {
long remaining = fileSize - position;
long size = Math.min(remaining, chunkSize);
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, position, size);
// 像操作内存一样操作文件
while (buffer.hasRemaining()) {
buffer.put(buffer.get());
}
position += size;
}
}
}方案三:零拷贝(只传输)
适合:只需要传输大文件(如文件服务器)。
java
public static void transferLargeFile(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
);
}
}
}带进度的大文件拷贝
java
public static void copyLargeFileWithProgress(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), 8 * 1024 * 1024);
BufferedOutputStream out = new BufferedOutputStream(
new FileOutputStream(dst), 8 * 1024 * 1024)
) {
byte[] buffer = new byte[8 * 1024 * 1024]; // 8MB
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%% (%d/%d MB)",
percent, copied / 1024 / 1024, totalSize / 1024 / 1024);
}
System.out.println();
}
}流式处理 CSV 大文件
java
public static void processLargeCsv(String path) throws IOException {
try (
BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(path), StandardCharsets.UTF_8), 1024 * 1024)
) {
String line;
int count = 0;
while ((line = reader.readLine()) != null) {
processRow(line);
count++;
if (count % 100000 == 0) {
System.out.println("已处理 " + count + " 行");
}
}
}
}
private static void processRow(String line) {
// 处理每一行
}选型建议
┌─────────────────────────────────────────────────────────────────┐
│ 大文件处理选型: │
│ │
│ 需要处理内容 → 分块读取 │
│ 需要随机访问 → 内存映射 │
│ 只需要传输 → 零拷贝 │
│ 大日志/CSV → 流式处理 │
└─────────────────────────────────────────────────────────────────┘总结
- 永远不要一次性读入大文件
- 分块读取 + 大缓冲区(1MB~8MB)
- 内存映射适合随机访问大文件
- 零拷贝适合只需要传输的大文件
