1. 程式人生 > >再來認識一下 Java 序列化

再來認識一下 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.Person
  • 0000 0000 0000 0001 類名後的 8 個位元組是一個長整數,即 serialVersionUID = 1L
  • 02 SC_SERIALIZABLE 標識位,說明這個類實現了 Serializable 介面。
  • 物件的欄位表
  • 0002 表示這個物件中有 2 個屬性
  • 49 即 I 表示 int,說明這是一個 32 位整數
  • 0003 表示屬性名的長度,即 3 位元組
  • 6167 65 表示屬性 age
  • 4c 即 L,表示引用型別,說明這個屬性是某個型別的引用
  • 0004 表示屬性名的長度,即 4 位元組
  • 6e61 6d65 表示屬性 name
  • 74 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 初始化後的年齡,轉換後即 30
  • 74 TC_STRING 表示後面是個字串
  • 0005 表示字串長度為 5
  • 4865 6e72 79 初始化之後的姓名,轉換後即 Henry

序列化的特性

在實際應用中,有些時候 不能使用預設序列化機制。比如,希望在序列化過程中忽略掉敏感資料。

本段重點討論 transient 和 static 之間的區別,並討論每個關鍵字的作用。

transient 關鍵字

當我們的一個欄位被宣告為 transient 後,預設序列化機制就會忽略掉該欄位的內容,不會被儲存。

static 關鍵字

序列化僅對特定的變數產生作用,但 static 修飾的變數並不特定於任何物件。因此,靜態變數不會參與序列化。

雖然用關鍵字可以避免序列化,但是當關鍵字組合使用的時候,也可能會失效。

transient 和 static 的規則

  1. 臨時變數在序列化過程中將被忽略。
  2. static 變數不會參與序列化。
  3. 如果在宣告本身期間對值進行了初始化,則靜態變數將被序列化。
  4. 如果一個變數同時包含 transient 和 static 關鍵字,並且該值在宣告期間被初始化,則它將被序列化。因為在這裡 transient 修飾符會被忽略,而 static 修飾符將執行操作。
  5. final 變數將被序列化。
  6. 如果一個變數同時包含 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 優化