1. 導論:為什麼要最佳化序列化?
在現代應用程式中,序列化無所不在——從網路協定到分散式快取,以及服務之間的資料交換。
序列化的速度與大小在此至關重要。若序列化很慢,應用程式在保存或載入資料時會開始「卡頓」,而網路或磁碟會被白白閒置。若物件過大,它們會佔用更多磁碟空間、在網路上傳輸更久,並對記憶體與頻寬造成額外負擔。
典型任務包括將大型物件圖保存到檔案或快取、以最小延遲在網路中傳遞物件,以及在多執行緒系統中快速序列化/反序列化資料。
結論很簡單:序列化的最佳化不是「高級功能」,而是打造高效且可擴充應用程式的必要做法。
2. 最佳化序列化資料的大小
排除不必要的資料:關鍵字 transient
預設會序列化所有欄位,除了被標記為 transient 的欄位。若某個欄位不需要保存(例如快取、暫存資料、服務參考),就將其標記為 transient:
public class User implements Serializable {
private String name;
private transient String sessionToken; // 不會被序列化
}
優點:
- 序列化物件更小。
- 檔案或網路中不會出現多餘/敏感資料。
手動序列化:Externalizable 介面
若需要完全掌控要序列化的內容與方式,請實作介面 Externalizable,並在方法 writeExternal/readExternal 中明確描述序列化:
public class Person implements Externalizable {
private String name;
private int age;
private transient String secret;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
// 不序列化 secret
}
@Override
public void readExternal(ObjectInput in) throws IOException {
name = in.readUTF();
age = in.readInt();
}
}
優點:
- 只序列化需要的欄位。
- 可以在不犧牲相容性的情況下變更序列化格式.
壓縮:壓縮序列化資料
序列化後的物件常常很大,尤其當它們包含重複的字串與大型集合。可以透過壓縮來減少大小。
使用 GZIPOutputStream 的範例:
try (ObjectOutputStream out = new ObjectOutputStream(
new GZIPOutputStream(new FileOutputStream("data.gz")))) {
out.writeObject(bigObject);
}
使用 ZipOutputStream 的範例:
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("data.zip"))) {
zip.putNextEntry(new ZipEntry("object"));
ObjectOutputStream out = new ObjectOutputStream(zip);
out.writeObject(bigObject);
out.flush();
zip.closeEntry();
}
優點:
- 檔案大小可能大幅縮小(對大型物件圖尤其明顯)。
- 降低網路傳輸流量。
缺點:
- 壓縮/解壓縮需要額外的時間(CPU)。
3. 最佳化序列化速度
緩衝:為什麼需要 BufferedOutputStream 與 BufferedInputStream
問題:
沒有緩衝時,每次呼叫 write() 或 read() 都會觸發對磁碟或網路的系統呼叫——這非常慢!
解法:
請使用具緩衝的串流:
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeObject(bigObject);
}
try (ObjectInputStream in = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("data.bin")))) {
Object obj = in.readObject();
}
優點:
- 顯著加速大型物件的寫入/讀取。
- 降低對磁碟/網路的訪問次數。
它如何運作?
緩衝會先在記憶體中累積資料,並以區塊寫出,而不是一個位元組一個位元組地寫。
快速複製:FileChannel.transferTo
若需要快速複製大型序列化檔案,請使用 NIO 與方法 transferTo:
try (FileChannel src = new FileInputStream("data.bin").getChannel();
FileChannel dest = new FileOutputStream("copy.bin").getChannel()) {
src.transferTo(0, src.size(), dest);
}
優點:
- 複製在作業系統層級進行,繞過 Java 中多餘的緩衝——對大檔案非常快。
4. 序列化效能剖析
簡單量測時間:System.nanoTime()
若要快速評估序列化效能,可以使用 System.nanoTime():
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeObject(bigObject);
}
long end = System.nanoTime();
System.out.println("序列化時間:" + (end - start) / 1_000_000 + " 毫秒");
優點:
- 簡單又快速。
- 可比較不同方案(有緩衝、無緩衝、有壓縮等)。
缺點:
- 結果可能因 GC 與背景程序而「飄動」。
- 不適合用來比較極其細微的差異。
精確剖析:JMH (Java Microbenchmark Harness)
若要更精準的量測,請使用 JMH——專門用於微基準的函式庫。
簡單基準範例:
@Benchmark
public void serializeWithBuffer() throws Exception {
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeObject(bigObject);
}
}
優點:
- 會考慮 JVM 預熱、GC 影響與作業系統雜訊。
- 提供可靠且可重現的結果。
缺點:
- 需要設定並理解 JMH 的方法論。
- 對於「憑感覺」的比較而言過於繁重。
5. 實作:比較序列化的時間與大小
我們來做個小型實驗:以不同方式序列化一個大型物件圖(例如包含巢狀集合的清單,共 100_000 個物件),並比較時間與檔案大小。
未使用緩衝與壓縮的序列化
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("未使用緩衝:" + (end - start) / 1_000_000 + " 毫秒,大小:" +
new File("data1.bin").length() + " 位元組");
使用緩衝的序列化
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data2.bin")))) {
out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("使用緩衝:" + (end - start) / 1_000_000 + " 毫秒,大小:" +
new File("data2.bin").length() + " 位元組");
使用壓縮(GZIP)的序列化
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
new GZIPOutputStream(new FileOutputStream("data3.gz")))) {
out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("使用壓縮:" + (end - start) / 1_000_000 + " 毫秒,大小:" +
new File("data3.gz").length() + " 位元組");
結果分析
在測試序列化時可以明顯看出緩衝與壓縮的影響。壓縮後的檔案通常會縮小 2–10 倍(確切倍數取決於資料結構)。使用緩衝會讓序列化明顯更快,而壓縮會稍微降低速度,但節省空間往往值得。
結論:對於大量資料務必使用緩衝;若檔案大小至關重要——就啟用壓縮。
6. 最佳化序列化時的常見錯誤
錯誤 #1: 未使用緩衝——大型物件的序列化會慢上好幾倍。
錯誤 #2: 序列化不必要或敏感的資料(例如密碼、暫時性權杖)——對這類欄位務必使用 transient。
錯誤 #3: 以為壓縮一定會加速序列化——事實上壓縮會縮小大小,但可能略微拖慢流程(在較弱的 CPU 上尤甚)。
錯誤 #4: 量測時間時未考慮 JVM 預熱與 GC 影響——若要精確的基準,請使用 JMH。
錯誤 #5: 只比較時間或只比較大小——務必同時觀察兩者,以找出最適合你情境的平衡。
GO TO FULL VERSION