ObjectOutputStream 是 Java 序列化的核心工具,廣泛用於檔案儲存、Socket 傳輸。但若使用不當,其內部優化機制會導致嚴重記憶體洩漏,尤其在長連線(Long-lived Connection)場景下。本文透過實測代碼揭示問題根源,並說明如何正確使用 reset() 避免記憶體溢位。
先看一個簡單的可序列化類別:
class MyObject implements Serializable {
private static final long serialVersionUID = -9163423175612080544L;
String str1;
String str2;
}
使用 ObjectOutputStream 寫入檔案:
FileOutputStream fos = new FileOutputStream("c:\\test.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
MyObject myObj = new MyObject();
myObj.str1 = "test1";
myObj.str2 = "test2";
oos.writeObject(myObj);
fos.close();
輸出內容包含類別資訊、欄位型別等中繼資料。這是 Java 序列化協議的一部分,確保反序列化時能重建物件。
若多次寫入同一物件實例:
for (int i = 0; i < 5; i++) {
oos.writeObject(myObj); // 同一實例
}
輸出中,類別描述只出現第一次,後續以引用代碼(如 #q ~)替代。這是 ObjectOutputStream 為減少傳輸開銷而維護的物件引用表。
即使每次寫入新建物件但內容相同,結果相同。但若每次新建物件且內容不同:
for (int i = 0; i < 5; i++) {
MyObject myObj = new MyObject();
myObj.str1 = "test1" + i;
myObj.str2 = "test2" + i;
oos.writeObject(myObj);
}
輸出顯示:第一次寫入完整類別描述,後續只寫入資料內容(以 #sq ~ 標記),類別中繼資料不再重複寫入。
為了實現上述優化,ObjectOutputStream 必須持有所有已寫入物件的強引用。在長連線(如 Socket 伺服器持續發送物件)場景下,這意味著:
OutOfMemoryError經 JProbe 追蹤確認:ObjectOutputStream 內部 handles 表持續增長,舊物件引用未釋放。
ObjectOutputStream.reset() 會:
ObjectInputStream 也會同步重置修改代碼:
for (int i = 0; i < 5; i++) {
MyObject myObj = new MyObject();
myObj.str1 = "test1" + i;
myObj.str2 = "test2" + i;
oos.writeObject(myObj);
oos.reset(); // 關鍵:釋放引用
}
輸出變化:每次寫入都重新輸出完整類別描述,結尾出現 #y(reset 標記)。雖增加傳輸開銷,但避免記憶體洩漏。
reset(),享受優化紅利。reset()。reset()(如每 100 次或記憶體達閾值),平衡效能與記憶體安全。ObjectOutputStream 的引用表優化雖善意,卻在長連線場景成了隱形殺手。理解其運作機制,靈活運用 reset(),才能在效能與穩定性間取得平衡。下次用到序列化傳輸時,別忘了問自己:這條連線會開多久?