CodeGym /課程 /JAVA 25 SELF /處理大型檔案:chunking、memory mapping

處理大型檔案:chunking、memory mapping

JAVA 25 SELF
等級 41 , 課堂 3
開放

1. Chunking — 以分塊方式讀取檔案

如同我們在前一講所討論的,chunking 讓你能以分塊方式處理檔案,而不是一次把整個檔案載入記憶體。當處理大量資料時尤其重要。若檔案只有 10 MB,通常不會有問題——可直接載入並用任何方式處理。但若檔案達到 10 GB,而實體記憶體只有 8 GB,再加上瀏覽器開了數十個分頁、IDE 也在執行呢?嘗試整檔讀入往往以悲劇收場:OutOfMemoryError、程式凍結,開發者欲哭無淚。

這樣的大型檔案在真實場景非常常見:伺服器一個月的日誌可達數十 GB,巨大的 CSV 檔包含上百萬行,而影片、壓縮檔與資料庫傾印更是有過之而無不及。

核心想法不變:不要試圖「一口把大象吃完」,而是逐塊處理。正是 chunking 讓你能安全、有效率地處理這類資料,將檔案拆成可控的區塊。

再談 chunking

Chunk(區塊、塊)就是檔案中某個固定大小的部分。與其一次全讀,我們可以每次讀取例如 4 MB(或 64 KB、或 1 MB——視情況而定)。

原則:

  • 開啟檔案的讀取串流。
  • 建立固定大小的位元組陣列作為緩衝區。
  • 在迴圈中將檔案讀入緩衝區,直到檔案結尾。
  • 逐一處理每個「區塊」。

範例:以分塊方式複製大型檔案

假設有一個超大的檔案需要複製。讓我們寫個「專業級」的程式來完成它。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BigFileCopy {
    public static void main(String[] args) throws IOException {
        String source = "bigfile.dat";
        String dest = "bigfile_copy.dat";
        int bufferSize = 4 * 1024 * 1024; // 4 MB

        try (FileInputStream in = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(dest)) {

            byte[] buffer = new byte[bufferSize];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
                // 可加入進度輸出或資料處理
            }
        }
        System.out.println("複製完成!");
    }
}

在 Java 中處理檔案通常使用標準串流 FileInputStreamFileOutputStream。良好實務是採用約 4 MB 的緩衝區——對現代磁碟而言,這樣的讀寫效率相當不錯。程式在迴圈中讀取檔案區塊並立刻寫入新檔,不會嘗試把整個檔案留在記憶體中。

此作法能節省記憶體、避免 OutOfMemoryError 等錯誤,並能處理幾乎任何大小的檔案,即使是 100 GB 以上也沒問題。

2. 用於資料處理的 chunking

許多時候,我們的任務不是單純複製檔案,而是要找到某行、計算出現次數、進行替換等。

範例:在大型文字檔中搜尋字串

如果檔案是文字檔,使用字元串流並逐行讀取會更方便:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BigFileSearch {
    public static void main(String[] args) throws IOException {
        String file = "biglog.txt";
        String keyword = "ERROR";
        int count = 0;

        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains(keyword)) {
                    count++;
                }
            }
        }
        System.out.println("找到 " + count + " 行含有 ERROR");
    }
}

為什麼這對數 GB 的檔案也有效?

  • BufferedReader 會以區塊方式讀檔(預設緩衝區為 8 KB,但可以自訂更大)。
  • 任一時刻,記憶體中只會保留一行文字。

Buffer size:該怎麼選?

黃金法則:緩衝區太小——磁碟 I/O 次數太多;太大——白白浪費記憶體。

  • 對現代 HDD/SSD,通常 64 KB–4 MB 表現良好。
  • 對網路儲存或非常快的 SSD——可再放大(8–16 MB)。
  • 對文字檔——可增大 BufferedReader 的緩衝區。

多做實驗!用不同緩衝區測量程式執行時間。有時放大緩衝區可提速 2–3 倍,有時則幾乎沒有影響。

3. Memory-mapped files(將檔案對映到記憶體)

這到底是什麼?

Memory mapping 是一種透過作業系統機制,將檔案「對映」到行程記憶體的方式。在 Java 中可使用 java.nio 套件裡的 MappedByteBuffer。檔案會像是一個大型位元組陣列,可直接操作,而不必顯式逐塊讀寫。

這對處理超大檔案特別有用。作業系統會自動載入所需的檔案部分,你可以像操作一般陣列一樣隨機存取檔案中的任意位置。Memory-mapped files 提供極高的隨機存取速度,例如需要從檔案不同位置快速讀取片段,又不想整檔載入時。

程式碼長什麼樣?

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MemoryMappedRead {
    public static void main(String[] args) throws Exception {
        String fileName = "bigfile.dat";
        try (RandomAccessFile file = new RandomAccessFile(fileName, "r");
             FileChannel channel = file.getChannel()) {

            long fileSize = channel.size();
            int chunkSize = 1024 * 1024 * 128; // 128 MB — 單一對映大小

            long position = 0;
            while (position < fileSize) {
                long size = Math.min(chunkSize, fileSize - position);
                MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, size);

                // 從 buffer 讀取資料,就像從陣列一樣
                for (int i = 0; i < size; i++) {
                    byte b = buffer.get(i);
                    // 處理位元組(例如尋找特定值)
                }

                position += size;
            }
        }
        System.out.println("透過 memory mapping 的讀取完成!");
    }
}

RandomAccessFileFileChannel 提供較低層級的檔案存取。呼叫 channel.map 會將檔案的一段對映到記憶體。存取資料則透過 MappedByteBuffer 緩衝區。

memory mapping 的優點是什麼?

  • 針對檔案不同部分的隨機存取非常快速。
  • 可處理大於可用實體記憶體的檔案(作業系統會自行載入所需頁面)。
  • 常見於現代資料庫、索引、大型日誌等場景。

缺點是什麼?

  • 並非總是適合寫入(尤其是網路檔案系統)。
  • 對映大小有限制(在 32 位元 JVM 中,單一對映通常不超過 2 GB)。
  • 若忘了關閉檔案,可能造成檔案「卡住」(尤其在 Windows)。
  • 不是所有檔案操作都會變快——若只是順序讀取,使用一般緩衝區往往不遜色。

4. 實用範例

範例 1:透過 memory mapping 在大型檔案中搜尋子字串

假設我們有一個 10 GB 的檔案,想要在其中尋找某段位元組序列(例如字串 "SECRET")。

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class MemoryMappedSearch {
    public static void main(String[] args) throws Exception {
        String fileName = "hugefile.bin";
        byte[] target = "SECRET".getBytes(StandardCharsets.UTF_8);

        try (RandomAccessFile file = new RandomAccessFile(fileName, "r");
             FileChannel channel = file.getChannel()) {

            long fileSize = channel.size();
            int chunkSize = 128 * 1024 * 1024; // 128 MB

            long position = 0;
            while (position < fileSize) {
                long size = Math.min(chunkSize, fileSize - position);
                MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, size);

                for (int i = 0; i < size - target.length; i++) {
                    boolean found = true;
                    for (int j = 0; j < target.length; j++) {
                        if (buffer.get(i + j) != target[j]) {
                            found = false;
                            break;
                        }
                    }
                    if (found) {
                        System.out.println("在位置 " + (position + i) + " 找到");
                        // 可選擇停止搜尋或繼續
                    }
                }
                position += size;
            }
        }
    }
}

請注意:
若子字串可能「跨越」兩個區塊,需要在區塊之間做長度為目標序列長度的重疊處理。

5. 有用的細節

什麼時候用 chunking,什麼時候用 memory mapping?

  • Chunking——適用於各類檔案(文字、二進位、日誌、壓縮檔)。對順序處理特別有效。
  • Memory mapping——對於隨機存取、巨大索引、資料庫、在超大檔案上的快速搜尋等極為有效。

若不確定該選哪種——先從 chunking 開始!Memory mapping 是強而有力但較「低階」的工具,需更謹慎地使用。

建議

  • 使用 try-with-resources 自動關閉串流與通道。
  • 不要同時開啟過多檔案:作業系統對開啟的描述元數量有上限。
  • 避免將對映範圍設得過大——這可能導致錯誤(尤其在 32 位元 JVM)。
  • 需要平行處理時可將檔案切成多個區塊並在不同執行緒處理(但要小心不要把磁碟打爆,或超出記憶體界限)。

6. 處理大型檔案時的常見錯誤

錯誤 1:嘗試把整個大型檔案載入記憶體。
這非常常見——尤其在新手身上。若檔案大於 1–2 GB,請使用 chunking 或逐行讀取,否則程式可能因 OutOfMemoryError 而崩潰。

錯誤 2:緩衝區太小。
512 位元組的緩衝區不是最佳化,而是效能「自殺」。請使用至少 64 KB 的緩衝區,甚至更大。

錯誤 3:忘記關閉串流或通道。
檔案描述元會一直佔用,檔案可能無法刪除或釋放,直到 JVM 重啟。請使用 try-with-resources

錯誤 4:不正確地使用 memory mapping。
若檔案在對映期間被其他行程修改,可能得到不一致的資料或錯誤。不要對經常變動的檔案使用 memory mapping。

錯誤 5:搜尋子字串時沒有處理區塊重疊。
若目標字串可能出現在兩個區塊的「接縫」上,務必在區塊之間做「以該字串長度為寬度」的重疊。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION