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

 

 

序列化方案

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

Java序列化

原理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  便可領取!

咱們下篇文章見!

感謝!

相關文章
相關標籤/搜索