CodeGym /課程 /JAVA 25 SELF /序列化的壓縮與效能剖析

序列化的壓縮與效能剖析

JAVA 25 SELF
等級 45, 課堂 4
開放

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. 最佳化序列化速度

緩衝:為什麼需要 BufferedOutputStreamBufferedInputStream

問題:
沒有緩衝時,每次呼叫 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() + " 位元組");

結果分析

在測試序列化時可以明顯看出緩衝與壓縮的影響。壓縮後的檔案通常會縮小 210 倍(確切倍數取決於資料結構)。使用緩衝會讓序列化明顯更快,而壓縮會稍微降低速度,但節省空間往往值得。

結論:對於大量資料務必使用緩衝;若檔案大小至關重要——就啟用壓縮。

6. 最佳化序列化時的常見錯誤

錯誤 #1: 未使用緩衝——大型物件的序列化會慢上好幾倍。

錯誤 #2: 序列化不必要或敏感的資料(例如密碼、暫時性權杖)——對這類欄位務必使用 transient

錯誤 #3: 以為壓縮一定會加速序列化——事實上壓縮會縮小大小,但可能略微拖慢流程(在較弱的 CPU 上尤甚)。

錯誤 #4: 量測時間時未考慮 JVM 預熱與 GC 影響——若要精確的基準,請使用 JMH

錯誤 #5: 只比較時間或只比較大小——務必同時觀察兩者,以找出最適合你情境的平衡。

1
任務
JAVA 25 SELF, 等級 45, 課堂 4
上鎖
資料檔案員:最大化重複資料的壓縮
資料檔案員:最大化重複資料的壓縮
1
任務
JAVA 25 SELF, 等級 45, 課堂 4
上鎖
遊戲開發者:評估儲存背包的效能
遊戲開發者:評估儲存背包的效能
1
問卷/小測驗
二進位序列化最佳化,等級 45,課堂 4
未開放
二進位序列化最佳化
二進位序列化最佳化
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION