再來認識一下 Java 序列化
前言
在面試中,Java 序列化被問到的幾率還是挺高的。所以搜集了 Java 序列化常見的問題,由淺入深的幫助大家進一步學習和理解。

序列化基礎知識
什麼是序列化?
Java 序列化是 JDK 1.1 中引入的特性之一。
總的來說,序列化講一個 Java 對象所描述的所有內容以文件 IO 的方式 存儲 或 傳輸 的過程。核心作用是對象狀態的保存和重建。
在這裡有兩個比較重要的概念:
- 序列化:把 Java 對象轉換為位元組碼的過程
- 反序列化:把位元組碼還原為 Java 對象的過程

為什麼要序列化 ?
因為 Java 對象是存放在 JVM 的 堆記憶體 中的,當 JVM 退出的時候,對象也就隨之銷毀。如果想 持久化 或進行 網路傳輸 對象數據時,那就必須把對象轉為電腦可以識別的位元組碼。
在以下場景中需要使用到序列化。
- 持久化數據:文件、資料庫、快取
- 網路傳輸:RMI (遠程調用 Remote Method Invocation)、RPC
如何實現序列化
在 Java 中,沒有關鍵字可以直接去定義一個所謂的 可持久化 對象。這就需要我們在程式碼中 顯示地 進行序列化和反序列化還原操作。
Serializable 介面
Serializable 介面是一個 標記介面,沒有方法或欄位。一旦實現了此介面,就標誌該類的對象就是可序列化的。
1、定義

2、序列化

3、反序列化

4、結果

5、如果不實現 Serializable 介面將無法進行序列化或反序列化

Externalizable 介面
Externalizable 繼承了 Serializable 介面,還定義了兩個抽象方法:writeExternal() 和 readExternal()。
如果開發人員使用 Externalizable 來實現序列化和反序列化,必須重寫 writeExternal() 和 readExternal() 方法。
因為實現 Externalizable 介面之後,基於 Serializable 介面的默認化序列化機制就會失效。

Serializable 和 Externalizable 的區別
Serializable | Externalizable |
---|---|
Java 支援比較完整,自動存儲必要資訊 | 需要開發人員自己完成 |
所有對象由 Java 統一保存,性能較低 | 開發人員決定哪個對象保存,可以提升速度 |
保存時佔用空間大,性能差 | 部分存儲,空間佔用可能較少,性能相對高 |
Java 序列化協議分析
下面這段位元組碼是保存在本地的位元組碼文件,接下來準備對這段位元組碼進行 拆分 和 講解 (只針對 Serializable)。
以下的位元組碼定義參考 java.io.ObjectStreamConstants
中的定義,如果有興趣,找到這個類,裡面有詳細的定義。

- JDk 序列化的魔數
aced
STREAM_MAGIC 魔數,用於標識當前文件的頭部0005
STREAM_VERSION 序列化協議版本號- 描述對象的類型資訊
73
TC_OBJECT 表示序列化的是一個普通 Java 對象 (Object 0x73,String 0x74,Array 0x75)72
TC_CLASSDESC 表示當前的對象的類型資訊0014
表示類名的長度,這段程式碼中是 0014 換算過來是 20 個位元組7374 6174 6963 4661 6374 6f72 792e 5065 7273 6f6e
表示類名,即 staticFactory.Person0000 0000 0000 0001
類名後的 8 個位元組是一個長整數,即 serialVersionUID = 1L02
SC_SERIALIZABLE 標識位,說明這個類實現了 Serializable 介面。- 對象的欄位表
0002
表示這個對象中有 2 個屬性49
即 I 表示 int,說明這是一個 32 位整數0003
表示屬性名的長度,即 3 位元組6167 65
表示屬性 age4c
即 L,表示引用類型,說明這個屬性是某個類型的引用0004
表示屬性名的長度,即 4 位元組6e61 6d65
表示屬性 name74
TC_STRING 表示後面是個字元串0012
表示字元串長度,即 18 位元組4c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b
即 Ljava/lang/String- 父類的描述資訊
78
TC_ENDBLOCKDATA 標誌所有的欄位類型資訊描述結束70
TC_NULL 代表 null,即沒有父類- 對象的屬性值
0000 001e
初始化後的年齡,轉換後即 3074
TC_STRING 表示後面是個字元串0005
表示字元串長度為 54865 6e72 79
初始化之後的姓名,轉換後即 Henry
序列化的特性
在實際應用中,有些時候 不能使用默認序列化機制。比如,希望在序列化過程中忽略掉敏感數據。
本段重點討論 transient 和 static 之間的區別,並討論每個關鍵字的作用。
transient 關鍵字
當我們的一個欄位被聲明為 transient 後,默認序列化機制就會忽略掉該欄位的內容,不會被保存。
static 關鍵字
序列化僅對特定的變數產生作用,但 static 修飾的變數並不特定於任何對象。因此,靜態變數不會參與序列化。
雖然用關鍵字可以避免序列化,但是當關鍵字組合使用的時候,也可能會失效。
transient 和 static 的規則
- 臨時變數在序列化過程中將被忽略。
- static 變數不會參與序列化。
- 如果在聲明本身期間對值進行了初始化,則靜態變數將被序列化。
- 如果一個變數同時包含 transient 和 static 關鍵字,並且該值在聲明期間被初始化,則它將被序列化。因為在這裡 transient 修飾符會被忽略,而 static 修飾符將執行操作。
- final 變數將被序列化。
- 如果一個變數同時包含 final 和 transient 關鍵字,那麼它就會被序列化。因為在這裡 transient 修飾符會被忽略,而 final 修飾符將執行操作。
下面用一段程式碼驗證一些。
1、定義一個實例化類

2、序列化

3、反序列化

4、輸出結果

重點:
- One 和 Two 為 null,根據規則 1,使用 tresient 修飾的變數不參與序列化
- Three 為 null,根據規則 2, static 變數不參與序列化
- Four 之所以為 V4,根據規則 3,僅在聲明期間初始化該值,靜態變數才會被序列化
- Five 為 null,根據規則4,因為它被 static 和 tresient 同時修飾,並且值在生命期間未初始化
- Six 之所以為 6,根據規則 4,如果同時 static 和 tresient 同時修飾,並且該值在聲明期間已初始化,那就會被序列化
- Seven 是 V7,根據規則 5,用 final 修飾的會被序列化
- Eight 之所以為 V8,根據規則 6,如果變數同時被 final 和 tresient 修飾,那就會被序列化
serialVersionUID 具體作用是什麼?
在序列化中,還有一個特別重要的步驟,需要指定 serialVersionUID 版本號。
如果反序列化使用的 Class 的版本號與序列化時候使用的不一致,則會報異常。
序列化版本號可以隨意的指定。
如果不指定,JVM 會 自己計算 一個版本號,但隨著 Class 的升級,就無法正確反序列化。
不指定版本號還有另一個明顯隱患,不利於 JVM 間的移植,可能 Class 文件沒有更改,但不同 JVM 可能計算的規則不一樣,這樣也會導致無法反序列化。
Java 序列化的缺陷
無法跨平台
現在的系統設計越來越多元化,項目里可能會用多種語言來編寫應用程式,比如 Java、C++、Python 同時配合使用。
而 Java 序列化只適用於基於 Java 語言實現的框架。其他語言大部分沒有使用 Java 的序列化框架。如果兩個基於不同語言編寫的應用程式相互通訊,那麼久無法實現兩個應用服務之間的序列化與反序列化。
容易被攻擊
對象是通過在 ObjectInputStream 上調用 readObject() 方法進行反序列化的,它可以將類路徑上幾乎所有實現了 Serializable 介面的對象都實例化。這意味著,在反序列化位元組流的過程中,該方法可以 執行任意類型的程式碼,這是非常危險的。
對於需要長時間進行反序列化的對象,不需要執行任何程式碼,也可以發起一次攻擊。攻擊者可以創建循環對象鏈,然後將序列化後的對象傳輸到程式中反序列化,這種情況會導致 hashCode 方法被調用次數呈次方爆髮式增長, 從而引發棧溢出異常。
序列化後的流太大
序列化後的二進位流大小能體現序列化的性能。序列化後的二進位數組越大,佔用的存儲空間就越多,存儲硬體的成本就越高。如果我們是進行網路傳輸,則佔用的頻寬就更多,這時就會影響到系統的吞吐量。
序列化的性能太差
Java 的序列化耗時比較大。序列化的速度也是體現序列化性能的重要指標,如果序列化的速度慢,就會影響網路通訊的效率,從而增加系統的響應時間。
序列化的其它問題
單例模式與序列化
首先拋出一個問題,單例模式真的能夠實現實例的唯一性嗎?
答案是否定的,很多人都知道反射可以 惡意破壞單例模式。其實除了反射以外,使用序列化與反序列化也同樣會破壞掉單例。比如下面這個單例:

上邊這種情況,其實已經破壞掉單例。因為序列化會通過反射調用無參構造器返回一個新的對象,從而破壞了單例模式,解決辦法就是 添加 readResolve() 方法,返回指定的對象。
巨人的肩膀
- 劉超 網路通訊優化之序列化:避免使用 Java 序列化 「極客時間」
- Java 優化