再來認識一下 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 優化
Tags: