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 中處理檔案通常使用標準串流 FileInputStream 與 FileOutputStream。良好實務是採用約 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 的讀取完成!");
}
}
RandomAccessFile 與 FileChannel 提供較低層級的檔案存取。呼叫 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:搜尋子字串時沒有處理區塊重疊。
若目標字串可能出現在兩個區塊的「接縫」上,務必在區塊之間做「以該字串長度為寬度」的重疊。
GO TO FULL VERSION