干貨系列性能篇之——序列化


 

 

序列化方案

  1. Java RMI采用的是Java序列化
  2. Spring Cloud采用的是JSON序列化
  3. Dubbo雖然兼容Java序列化,但默認使用的是Hessian序列化

Java序列化

原理

 

Serializable

  1. JDK提供了輸入流對象ObjectInputStream和輸出流對象ObjectOutputStream
  2. 它們只能對實現了Serializable接口的類的對象進行序列化和反序列化
// 只能對實現了Serializable接口的類的對象進行序列化
// java.io.NotSerializableException: java.lang.Object
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(new Object());
oos.close();

transient

  1. ObjectOutputStream的默認序列化方式,僅對對象的非transient的實例變量進行序列化
  2. 不會序列化對象的transient的實例變量,也不會序列化靜態變量
@Getter
public class A implements Serializable {
 private transient int f1 = 1;
 private int f2 = 2;
 @Getter
 private static final int f3 = 3;
}
// 序列化
// 僅對對象的非transient的實例變量進行序列化
A a1 = new A();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(a1);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
A a2 = (A) ois.readObject();
log.info("f1={}, f2={}, f3={}", a2.getF1(), a2.getF2(), a2.getF3()); // f1=0, f2=2, f3=3
ois.close();

serialVersionUID

  1. 在實現了Serializable接口的類的對象中,會生成一個serialVersionUID的版本號
  2. 在反序列化過程中用來驗證序列化對象是否加載了反序列化的類
  3. 如果是具有相同類名的不同版本號的類,在反序列化中是無法獲取對象的
@Data
@AllArgsConstructor
public class B implements Serializable {
 private static final long serialVersionUID = 1L;
 private int id;
}
@Test
public void test3() throws Exception {
 // 序列化
 B b1 = new B(1);
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
 oos.writeObject(b1);
 oos.close();
}
@Test
public void test4() throws Exception {
 // 如果先將B的serialVersionUID修改為1,直接反序列化磁盤上的文件,會報異常
 // java.io.InvalidClassException: xxx.B; local class incompatible: stream classdesc serialVersionUID = 0, local class serialVersionUID = 1
 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
 B b2 = (B) ois.readObject();
 ois.close();
}

writeObject/readObject

具體實現序列化和反序列化的是writeObject和readObject

@Data
@AllArgsConstructor
public class Student implements Serializable {
 private long id;
 private int age;
 private String name;
 // 只序列化部分字段
 private void writeObject(ObjectOutputStream outputStream) throws IOException {
 outputStream.writeLong(id);
 outputStream.writeObject(name);
 }
 // 按序列化的順序進行反序列化
 private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
 id = inputStream.readLong();
 name = (String) inputStream.readObject();
 }
}
Student s1 = new Student(1, 12, "Bob");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(s1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
Student s2 = (Student) ois.readObject();
log.info("s2={}", s2); // s2=Student(id=1, age=0, name=Bob)
ois.close();

writeReplace/readResolve

  1. writeReplace:用在序列化之前替換序列化對象
  2. readResolve:用在反序列化之后對返回對象進行處理
// 反序列化會通過反射調用無參構造器返回一個新對象,破壞單例模式
// 可以通過readResolve()來解決
public class Singleton1 implements Serializable {
 private static final Singleton1 SINGLETON_1 = new Singleton1();
 private Singleton1() {
 }
 public static Singleton1 getInstance() {
 return SINGLETON_1;
 }
}
Singleton1 s1 = Singleton1.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(s1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
Singleton1 s2 = (Singleton1) ois.readObject();
log.info("{}", s1 == s2); // false
ois.close();

public class Singleton2 implements Serializable {
 private static final Singleton2 SINGLETON_2 = new Singleton2();
 private Singleton2() {
 }
 public static Singleton2 getInstance() {
 return SINGLETON_2;
 }
 public Object writeRepalce() {
 // 序列化之前,無需替換
 return this;
 }
 private Object readResolve() {
 // 反序列化之后,直接返回單例
 return getInstance();
 }
}
Singleton2 s1 = Singleton2.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(s1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
Singleton2 s2 = (Singleton2) ois.readObject();
log.info("{}", s1 == s2); // true
ois.close();

缺陷

無法跨語言

Java序列化只適用於基於Java語言實現的框架

易被攻擊

1.Java序列化是不安全的

  • Java官網:對不信任數據的反序列化,本質上來說是危險的,應該予以回避

2.ObjectInputStream.readObject()

  • 將類路徑上幾乎所有實現了Serializable接口的對象都實例化!!
  • 這意味着:在反序列化字節流的過程中,該方法可以執行任意類型的代碼,非常危險

3.對於需要長時間進行反序列化的對象,不需要執行任何代碼,也可以發起一次攻擊

  • 攻擊者可以創建循環對象鏈,然后將序列化后的對象傳輸到程序中進行反序列化
  • 這會導致haseCode方法被調用的次數呈次方爆發式增長,從而引發棧溢出異常

4.很多序列化協議都制定了一套數據結構來保存和獲取對象,如JSON序列化、ProtocolBuf

  • 它們只支持一些基本類型和數組類型,可以避免反序列化創建一些不確定的實例
int itCount = 27;
Set root = new HashSet();
Set s1 = root;
Set s2 = new HashSet();
for (int i = 0; i < itCount; i++) {
 Set t1 = new HashSet();
 Set t2 = new HashSet();
 t1.add("foo"); // 使t2不等於t1
 s1.add(t1);
 s1.add(t2);
 s2.add(t1);
 s2.add(t2);
 s1 = t1;
 s2 = t2;
}
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
oos.writeObject(root);
oos.close();
long start = System.currentTimeMillis();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH));
ois.readObject();
log.info("take : {}", System.currentTimeMillis() - start);
ois.close();
// itCount - take
// 25 - 3460
// 26 - 7346
// 27 - 11161

序列化后的流太大

1.序列化后的二進制流大小能體現序列化的能力

2.序列化后的二進制數組越大,占用的存儲空間就越多,存儲硬件的成本就越高

  • 如果進行網絡傳輸,則占用的帶寬就越多,影響到系統的吞吐量

3.Java序列化使用ObjectOutputStream來實現對象轉二進制編碼,可以對比BIO中的 ByteBuffer實現的二進制編碼

@Data
class User implements Serializable {
 private String userName;
 private String password;
}
User user = new User();
user.setUserName("test");
user.setPassword("test");
// ObjectOutputStream
ByteArrayOutputStream os = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(user);
log.info("{}", os.toByteArray().length); // 107
// NIO ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
byte[] userName = user.getUserName().getBytes();
byte[] password = user.getPassword().getBytes();
byteBuffer.putInt(userName.length);
byteBuffer.put(userName);
byteBuffer.putInt(password.length);
byteBuffer.put(password);
byteBuffer.flip();
log.info("{}", byteBuffer.remaining()); // 16

序列化速度慢

  1. 序列化速度是體現序列化性能的重要指標
  2. 如果序列化的速度慢,就會影響網絡通信的效率,從而增加系統的響應時間
int count = 10_0000;
User user = new User();
user.setUserName("test");
user.setPassword("test");
// ObjectOutputStream
long t1 = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
 ByteArrayOutputStream os = new ByteArrayOutputStream();
 ObjectOutputStream oos = new ObjectOutputStream(os);
 oos.writeObject(user);
 oos.flush();
 oos.close();
 byte[] bytes = os.toByteArray();
 os.close();
}
long t2 = System.currentTimeMillis();
log.info("{}", t2 - t1); // 731
// NIO ByteBuffer
long t3 = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
 ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
 byte[] userName = user.getUserName().getBytes();
 byte[] password = user.getPassword().getBytes();
 byteBuffer.putInt(userName.length);
 byteBuffer.put(userName);
 byteBuffer.putInt(password.length);
 byteBuffer.put(password);
 byteBuffer.flip();
 byte[] bytes = new byte[byteBuffer.remaining()];
}
long t4 = System.currentTimeMillis();
log.info("{}", t4 - t3); // 182

ProtoBuf

  1. ProtoBuf是由Google推出且支持多語言的序列化框架
  • 在序列化框架性能測試報告中,ProtoBuf無論編解碼耗時,還是二進制流壓縮大小,都表現很好
  1. ProtoBuf以一個.proto后綴的文件為基礎,該文件描述了字段以及字段類型,通過工具可以生成不同語言的數據結構文件
  2. 在序列化該數據對象的時候,ProtoBuf通過.proto文件描述來生成Protocol Buffers格式的編碼

存儲格式

  1. Protocol Buffers是一種輕便高效的結構化數據存儲格式
  2. Protocol Buffers使用T-L-V(標識-長度-字段值)的數據格式來存儲數據
  • T代表字段的正數序列(tag)
  • Protocol Buffers將對象中的字段與正數序列對應起來,對應關系的信息是由生成的代碼來保證的
  • 在序列化的時候用整數值來代替字段名稱,傳輸流量就可以大幅縮減
  • L代表Value的字節長度,一般也只占用一個字節
  • V代表字段值經過編碼后的值
  1. 這種格式不需要分隔符,也不需要空格,同時減少了冗余字段名

編碼方式

 

1.ProtoBuf定義了一套自己的編碼方式,幾乎可以映射Java/Python等語言的所有基礎數據類型

2.不同的編碼方式可以對應不同的數據類型,還能采用不同的存儲格式

3.對於Varint編碼的數據,由於數據占用的存儲空間是固定的,因此不需要存儲字節長度length,存儲方式采用T-V

4.Varint編碼是一種變長的編碼方式,每個數據類型一個字節的最后一位是標志位(msb)

  • 0表示當前字節已經是最后一個字節
  • 1表示后面還有一個字節

5.對於int32類型的數字,一般需要4個字節表示,如果采用Varint編碼,對於很小的int類型數字,用1個字節就能表示

  • 對於大部分整數類型數據來說,一般都是小於256,所以這樣能起到很好的數據壓縮效果

編解碼

  1. ProtoBuf不僅壓縮存儲數據的效果好,而且編解碼的性能也是很好的
  2. ProtoBuf的編碼和解碼過程結合.proto文件格式,加上Protocol Buffers獨特的編碼格式
  • 只需要簡單的數據運算以及位移等操作就可以完成編碼和解碼

我是小架,需要Java學習進階架構資料。加我的交流群

772300343  即可領取!

我們下篇文章見!

感謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM