文件讀寫是後端開發高頻操作,選擇正確的 IO 模式能帶來數量級性能差異。本文通過基準測試對比傳統 Stream、NIO ByteBuffer 及內存映射 MappedByteBuffer 三種方式,揭示 MappedByteBuffer 為何能以 112ms 完勝 1000ms 的 Stream。
2000 * 2000 * 4 bytes)@Test
public void useStream() throws IOException {
long start = System.currentTimeMillis();
int num = 2000 * 2000;
// 寫入
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(FILE)))) {
for (int i = 0; i < num; i++) out.writeInt(i);
}
// 讀取
int data = 0;
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream(FILE)))) {
while (true) data = in.readInt();
} catch (EOFException ignored) {}
System.out.println("Stream 耗時: " + (System.currentTimeMillis() - start) + "ms");
}
@Test
public void useNioByteBuffer() throws IOException {
long start = System.currentTimeMillis();
int num = 2000 * 2000;
int capacity = num * 5; // 預留空間
// 寫入
try (FileOutputStream fos = new FileOutputStream(FILE);
FileChannel fc = fos.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(capacity);
for (int i = 0; i < num; i++) buffer.putInt(i);
buffer.flip();
fc.write(buffer);
}
// 讀取
try (FileInputStream fis = new FileInputStream(FILE);
FileChannel fc = fis.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(capacity);
fc.read(buffer);
buffer.flip();
while (buffer.hasRemaining()) buffer.getInt();
}
System.out.println("ByteBuffer 耗時: " + (System.currentTimeMillis() - start) + "ms");
}
@Test
public void useMappedByteBuffer() throws IOException {
long start = System.currentTimeMillis();
int num = 2000 * 2000;
// 寫入
try (RandomAccessFile raf = new RandomAccessFile(FILE, "rw");
FileChannel fc = raf.getChannel()) {
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, num * 4L);
for (int i = 0; i < num; i++) mbb.putInt(i);
mbb.force(); // 強制刷新到磁盤
}
// 讀取
try (RandomAccessFile raf = new RandomAccessFile(FILE, "rw");
FileChannel fc = raf.getChannel()) {
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, raf.length());
while (mbb.hasRemaining()) mbb.getInt();
}
System.out.println("MappedByteBuffer 耗時: " + (System.currentTimeMillis() - start) + "ms");
}
| 方式 | 平均耗時 | 核心優勢 | 適用場景 |
|---|---|---|---|
| Stream (Buffered) | ~1000 ms | API 簡單,兼容性最好 | 小文件、配置文件、兼容舊系統 |
| NIO ByteBuffer | ~220 ms | 零拷貝(用戶態↔內核態)、通道批量操作 | 中大文件、網絡傳輸、自定義協議解析 |
| MappedByteBuffer | ~112 ms | 虛擬內存映射,OS 頁面調度,用戶態直接操作物理頁 | 大文件(>100MB)、隨機讀寫、高吞吐日誌/索引 |
read()/write() 系統調用及內核緩衝區↔用戶緩衝區拷貝。maxDirectMemorySize)。大文件需分段映射。-Xmx 不限制,易導致 OOM Killer 殺進程。生產環境建議監控 Direct Memory 並設置 -XX:MaxDirectMemorySize。MappedByteBuffer 無 close(),依賴 Cleaner 或反射釋放,長生命周期映射需手動 sun.misc.Cleaner.clean()。FileLock。if (fileSize < 1MB) -> Stream (Buffered) // 開發效率最高
else if (fileSize < 100MB) -> NIO ByteBuffer // 平衡性能與可控性
else if (sequentialAccess) -> MappedByteBuffer // 大文件順序讀寫極致性能
else -> MappedByteBuffer + 分段映射 // 隨機訪問大文件