java序列化总结

为什么需要序列化

  • 数据持久化(如session信息存储到redis)或在网络上传输(如RPC远程调用)

序列化要考虑的因素

  • 性能:速度越快越好
  • 序列化后字节大小:字节越小越好,节省带宽和存储器空间
  • 兼容性:类的信息发生变化,旧的序列化数据是否能正常用新类反序列化,或者反之。如果序列化内容是放在内存并且每次发版(停服发版)都会清空,那么可以不考虑兼容,否则兼容性就要考虑。灰度发布之类的也要考虑兼容性。这里说的兼容性是指加减字段,不包括更改字段类型。

常见序列化方式

  • JDK自带的ObjectInputStream和ObjectOutputStream,需要实现Serializable,需要兼容的话要写死servialVersionUID。性能低、体积大。
  • 各类json(jackson, gson, fastjson),性能比jdk稍高,体积也稍小,对人友好,基本所有主流语言都支持,跨语言性非常好。兼容性好。但是类和字段的信息没有序列化进去,在反序列化的时候需要指定类名。
  • hessian:性能和字节比jdk好,兼容性差。如果子类和父类有相同的属性名,那么在反序列化后会丢失字节,原因:hessian先写子类Field,再写父类Field,写值的顺序也一样,因为父类Field的值一般都是null,所以在反序列化的时候,总是把最后的父类的null值覆盖掉子类的值,具体原因参考:https://www.cnblogs.com/yfyzy/p/7197679.html。hessian的一些类不是public,不能继承,如果要改的话只能改源码了。
  • hessian-lite:阿里dubbo项目里默认用的序列化协议,改自hessian,他解决了字节丢失问题,就是在获取所有Field后做下reverse操作,颠倒了Field的顺序。但是经过测试发现heissian-lite速度太慢了,见issue:https://github.com/dubbo/hessian-lite/issues/10
  • kryo:速度和性能都很好,默认不兼容,不过通过设置CompatibleFieldSerializcer就能支持兼容,但是也不允许父类和子类有相同名字的属性,可以通过继承过滤掉同名属性。kryo可以参考官方文档,https://github.com/EsotericSoftware/kryo#compatiblefieldserializer-settings,很详细的。
  • fst:性能和字节大小都是最优的,可惜兼容性要在字段上加@Version,只能增字段不能删,对业务开发侵入太大,如果不考虑兼容的话可以考虑用fst。参考:https://blog.csdn.net/dutlxq2014/article/details/86698268。wiki:https://github.com/RuedigerMoeller/fast-serialization/wiki
  • 需要静态编译的,如果protobuf, thrift,适合内部系统之间RPC,本文不涉及这部分。

kryo目前的bug

  • kryo不要每次都new Kryo(),这样性能太差,需要用ThreadLocal或池化存储kryo实例,不过目前发行版池化有个bug:https://github.com/EsotericSoftware/kryo/issues/642,每次池里取不到都会new一个出来,在还到池里的时候,如果池满了就会抛queue full异常。目前kryo池化还有一个bug,参考:https://github.com/EsotericSoftware/kryo/issues/664。只能自己实现池化。
  • 序列化后如果bean的字段改了类型会导致jvm crash,虽说字段改类型不应该,但是导致jvm crash也是一个大问题。参考:https://github.com/EsotericSoftware/kryo/issues/663,能反序列化成功就是因为从序列化字节里拿到原来的类型,然后通过unsafe直接写内存。

性能和字节大小对比

SerializeBenchmarkTest3测试类,对几种序列化方式进行了测试:
测试数据:

private Person getPerson() {
        Person person = new Person();
        person.setId(123L);
        person.setName("你好啊");
        person.setMarried(true);
        person.setAge(22);
        person.setDigits(Arrays.asList(1L, 3L, 100L));
        Map scoreMap = new LinkedHashMap<>();
        scoreMap.put("chinese", 90d);
        scoreMap.put("english", 80.5d);
        person.setScores(scoreMap);
        Book book = new Book();
        book.setId(99L);
        book.setName("代码大全");
        book.setPrice(56.00d);
        person.setBook(book);

        int friendsCount = 1000;
        List friends = new ArrayList<>(friendsCount);
        for (int i = 0; i < friendsCount; i++) {
            Person friend = new Person();
            friend.setId(Long.valueOf(i));
            friend.setName(String.valueOf("我的朋友" + i));
            friend.setMarried(i % 2 == 0 ? true : false);
            friends.add(friend);
        }
        person.setFriends(friends);
        return person;
    }

test1方法测试了序列化后自己大小和md5,测试结果如下:

2019-03-23 15:22:04,916 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.jdkPerformance(54) - jdk序列化后长度:52797, 前后长度一致:true, md5一致:true,对象equals:true
2019-03-23 15:22:05,226 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.jsonPerformance(71) - json序列化后长度:59457, 前后长度一致:true, md5一致:true,对象equals:true
2019-03-23 15:22:05,309 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.hessian2Performance(88) - hessian2序列化后长度:26124, 前后长度一致:false, md5一致:false,对象equals:false
2019-03-23 15:22:05,380 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.hessianLitePerformance(105) - hessian-lite序列化后长度:26144, 前后长度一致:false, md5一致:false,对象equals:true
2019-03-23 15:22:05,661 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.kryoPerformance(122) - kryo序列化后长度:28101, 前后长度一致:true, md5一致:true,对象equals:true
2019-03-23 15:22:05,696 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.fstPerformance(139) - fst序列化后长度:33839, 前后长度一致:true, md5一致:true,对象equals:true

由于Person类继承了Human类,2个类都有同名属性id,hessian2在序列化的时候存在bug导致丢失数据,奇怪的是hessian-lite虽然解决了这个bug,但是前后序列化字节长度却不相等。
从上面结果可以看出,在小数据量场景下,hessian2及hessian-lite在体积上占有小优势,kryo、fst次之,jdk和json最差。

然后对上面的数据做10000次序列化和反序列化,结果如下:

14次YGC
13.658: [GC (Allocation Failure) [PSYoungGen: 682646K->233K(691200K)] 686763K->4350K(2089472K), 0.0018965 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-03-23 15:31:22,073 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.jdkPerformance(61) - jdk序列化、反序列化10000次耗时13699

9次YGC
19.258: [GC (Allocation Failure) [PSYoungGen: 687902K->318K(693248K)] 692530K->4946K(2091520K), 0.0006095 secs] [Times: user=0.06 sys=0.00, real=0.00 secs] 
2019-03-23 15:31:27,321 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.jsonPerformance(78) - json序列化、反序列化10000次耗时5245

5次YGC
24.824: [GC (Allocation Failure) [PSYoungGen: 689776K->121K(694784K)] 694539K->4909K(2093056K), 0.0007943 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-03-23 15:31:32,671 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.hessian2Performance(95) - hessian2序列化、反序列化10000次耗时5349

11次YGC
35.886: [GC (Allocation Failure) [PSYoungGen: 694368K->64K(696320K)] 699315K->5011K(2094592K), 0.0073285 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
2019-03-23 15:31:43,688 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.hessianLitePerformance(112) - hessian-lite序列化、反序列化10000次耗时11017


3次YGC
39.634: [GC (Allocation Failure) [PSYoungGen: 694880K->64K(696832K)] 700003K->5203K(2095104K), 0.0007231 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-03-23 15:31:48,061 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.kryoPerformance(129) - kryo序列化、反序列化10000次耗时4373


3次YGC
43.816: [GC (Allocation Failure) [PSYoungGen: 694945K->193K(696832K)] 700308K->5556K(2095104K), 0.0007227 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2019-03-23 15:31:51,626 WARN  [main] c.y.o.s.SerializeBenchmarkTest3.fstPerformance(146) - fst序列化、反序列化10000次耗时3564

jvm参数:-Xms2g -Xmx2g -XX:+PrintGCTimeStamps -XX:+PrintGCDetails

可以看出fst最快,kryo次之,json、hessian2速度还不错,但是hessian-lite和jdk基本上一样慢。

最佳实践

  • 序列化的类最好实现Serializable接口,并写死serialVersionUID
  • 序列化的类可以加减字段,但是最好不要改字段类型
  • 如果是开放出去的api,最好采用可读性好、适合web的json,兼容性也好,和语言没有耦合,就是浪费带宽
  • 如果是内部RPC,可以采用fst和kryo,或者protobuf, thrift。如果要兼容多版本,fst就不太适合
  • 如果有持久化需求,需要考虑到兼容性,可以采用kryo, json

序列化工具类:

static MzKryoPool kryoPool = new MzKryoPool(100);
    static FSTConfiguration fst = FSTConfiguration.createDefaultConfiguration();

    public static  byte[] serializeWithJdk(T object) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4096);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(object);
            byte[] bytes = byteArrayOutputStream.toByteArray();
            objectOutputStream.close();
            return bytes;
        } catch (IOException e) {
            throw new OperationException("serialize with jdk fail: " + e.getMessage(), e);
        }
    }

    public static Object deserializeWithJdk(byte[] bytes) {
        try {
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            Object object = objectInputStream.readObject();
            objectInputStream.close();
            return object;
        } catch (ClassNotFoundException | IOException e) {
            throw new OperationException("deserialize with jdk fail: " + e.getMessage(), e);
        }
    }

    public static byte[] serializeWithJson(Object object) {
        return JSON.toJSONBytes(object);
    }

    public static  T deserializeWithJson(byte[] bytes, Class cls) {
        return JSON.parseObject(bytes, cls);
    }

    public static byte[] serializeWithHessian2(Object object) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4096);
            Hessian2Output hessianOutput = new Hessian2Output(byteArrayOutputStream);
            hessianOutput.startMessage();
            hessianOutput.writeObject(object);
            hessianOutput.completeMessage();
            hessianOutput.close();
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            throw new OperationException("serialize with hessian2 fail: " + e.getMessage(), e);
        }
    }

    public static Object deserializeWithHessian2(byte[] bytes) {
        try {
            Hessian2Input hessian2Input = new Hessian2Input(new ByteArrayInputStream(bytes));
            hessian2Input.startMessage();
            Object o = hessian2Input.readObject();
            hessian2Input.completeMessage();
            hessian2Input.close();
            return o;
        } catch (IOException e) {
            throw new OperationException("deserialize with hessian2 fail: " + e.getMessage(), e);
        }
    }

    public static byte[] serializeWithHessianLite(Object object) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4096);
            com.alibaba.com.caucho.hessian.io.Hessian2Output hessian2Output = new com.alibaba.com.caucho.hessian.io.Hessian2Output(byteArrayOutputStream);
            hessian2Output.startMessage();
            hessian2Output.writeObject(object);
            hessian2Output.completeMessage();
            hessian2Output.close();
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            throw new OperationException("serialize with hessian-lite fail: " + e.getMessage(), e);
        }
    }

    public static Object deserializeWithHessianLite(byte[] bytes) {
        try {
            com.alibaba.com.caucho.hessian.io.Hessian2Input hessian2Input = new com.alibaba.com.caucho.hessian.io.Hessian2Input(new ByteArrayInputStream(bytes));
            hessian2Input.startMessage();
            Object o = hessian2Input.readObject();
            hessian2Input.completeMessage();
            hessian2Input.close();
            return o;
        } catch (IOException e) {
            throw new OperationException("deserialize with hessian-lite fail: " + e.getMessage(), e);
        }
    }

    public static byte[] serializeWithKryo(Object obj) {
        Kryo kryo = kryoPool.obtain();
        //initial 4k, max 10M
        try (Output output = new Output(4096, 10 * 1024 * 1024);) {
            kryo.writeClassAndObject(output, obj);
            return output.toBytes();
        } catch (Exception e) {
            throw new OperationException("deserialize with kryo fail: " + e.getMessage(), e);
        } finally {
            kryoPool.free(kryo);
        }
    }

    public static Object deserializeWithKryo(byte[] bytes) {
        Kryo kryo = kryoPool.obtain();
        try (Input input = new Input(bytes)) {
            return kryo.readClassAndObject(input);
        } catch (Exception e) {
            throw new OperationException("deserialize with kryo fail: " + e.getMessage(), e);
        } finally {
            kryoPool.free(kryo);
        }
    }

    public static byte[] serializeWithFst(Object obj) {
        return fst.asByteArray(obj);
    }

    public static Object deserializeWithFst(byte[] bytes) {
        return fst.asObject(bytes);
    }

你可能感兴趣的:(java序列化总结)