首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 文件 IO 性能大比拼:Stream vs NIO ByteBuffer vs MappedByteBuffer

     編輯:Java 相關知識

文件讀寫是後端開發高頻操作,選擇正確的 IO 模式能帶來數量級性能差異。本文通過基準測試對比傳統 Stream、NIO ByteBuffer 及內存映射 MappedByteBuffer 三種方式,揭示 MappedByteBuffer 為何能以 112ms 完勝 1000ms 的 Stream。

一、測試環境與數據規模

  • 數據量:400 萬整數(約 16 MB,2000 * 2000 * 4 bytes)
  • 操作:順序寫入 + 順序讀取
  • JDK 版本:1.8+,Linux/macOS 文件系統緩存已預熱

二、三種實現方式核心代碼

1. 傳統 Stream(帶緩衝)

@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");
}

2. NIO ByteBuffer(堆內緩衝區)

@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");
}

3. MappedByteBuffer(內存映射文件)

@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 msAPI 簡單,兼容性最好小文件、配置文件、兼容舊系統
NIO ByteBuffer~220 ms零拷貝(用戶態↔內核態)、通道批量操作中大文件、網絡傳輸、自定義協議解析
MappedByteBuffer~112 ms虛擬內存映射,OS 頁面調度,用戶態直接操作物理頁大文件(>100MB)、隨機讀寫、高吞吐日誌/索引

四、原理深度解析

為什麼 MappedByteBuffer 最快?

  1. 零拷貝:文件頁直接映射到進程虛擬地址空間,讀寫即內存操作,省去 read()/write() 系統調用及內核緩衝區↔用戶緩衝區拷貝。
  2. 頁面級調度:OS 自動處理頁面缺失、預讀、回寫,利用空閒內存作為頁面緩存,無需應用層管理 Buffer 大小。
  3. 隨機訪問 O(1):指針運算即可定位任意偏移量,適合索引、稀疏讀寫。

避坑指南

  • 映射大小限制:32 位 JVM 最大映射 1.5~2GB;64 位理論無上限,但受虛擬內存區域限制(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 + 分段映射 // 隨機訪問大文件
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved