zoukankan      html  css  js  c++  java
  • 干货系列性能篇之——序列化

    序列化方案

    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  即可领取!

    我们下篇文章见!

    感谢!

  • 相关阅读:
    wget: command not found
    小程序循环多个picker选择器,实现动态增、减
    小程序 picker 多列选择器 数据动态获取
    有关https有的网站可以访问有的访问不了的问题
    微信小程序填坑之路
    linux如何搭建sftp服务器
    微信小程序模板中使用循环
    C#学习笔记(20)——使用IComparer(自己写的)
    C#学习笔记(19)——使用IComparer(百度文库)
    C#学习笔记(18)——C#构造函数中this和base的使用
  • 原文地址:https://www.cnblogs.com/sevencutekk/p/11586355.html
Copyright © 2011-2022 走看看