Java序列化与ProtocalBuffer序列化之深入分析(转)

时间:2021-10-26 17:34:04

最近想把java里执行数据和树全部记录下来,进行回放. 需要动态地通过反射将对象序列化和反序列化. 遇到 execute(List<xxxParam> params) ;

可能就无法通过反射 依赖json反序列化回来,只能将具有自描述的java序列化回来.

rcp框架的接口List<>参数, 反序列化怎么搞的? 用的是java序列化,所以需要继承 Serializable接口

以前保存过一篇文章.

Java序列化与ProtocalBuffer序列化之深入分析(转)

Java序列化与ProtocalBuffer序列化之深入分析(转)(2013-07-31 20:26:21)Java序列化与ProtocalBuffer序列化之深入分析(转)转载
标签:

序列化

java

protocalbuffer

it

分类:Tech
原文链接:http://kangsg219.iteye.com/blog/904762


今天看了《Java序列化与ProtocalBuffer序列化之深入分析》,感觉有所收获。原文中对ObjectStreamField中关于属性类型与字符表示的映射没有指出来,在原帖中回复了作者,这里稍作修改并转发

 

从一个简单对象的序列化内容来看java序列化与ProtocalBuffer序列化机制的不同之处以及优劣所在。对象准备如下:

 

父类BaseUserDO.java(gettersetter方法省去)

package serialize.compare;

public class BaseUserDO implements Serializable{

    private static final long serialVersionUID =5699113544108250452L;

    private int pid;

}

 

子类UserDO.java继承上面的父类

package serialize.compare;

public class UserDO extends BaseUserDO{

    private static final long serialVersionUID =6532984488602164707L;

    private int id;

    private String name;

}

 

New一个准备序列化的对象

UserDO user= new UserDO();

user.setPid(10);

user.setId(300);

user.setName("JavaSerialize ");//PbSerialize

 

 

Java序列化生成的16进制字节码共151个,内容如下:

 

AC ED 0005 73 72 0018 7365 72 69 61 6C 69 7A 65 2E 63 6F 6D 70 61 72 65 2E 55 73 65 72 444F 5AA9 D2 C7 76 44 DD E3 02 0002 49 0002 6964 4C 0004 6E61 6D 65 74 0012 4C6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 673B 78 72 001C 7365 72 69 61 6C 69 7A 65 2E 63 6F 6D 70 61 72 65 2E 42 61 73 65 5573 65 72 44 4F 4F17 51 92 BB 30 95 54 02 0001 49 0003 7069 64 78 70 0000 00 0A 0000 01 2C 74 000D 4A61 76 61 53 65 72 69 61 6C 69 7A 65

 

ProtocalBuffer序列化生成的16进制字节码只有18个,内容如下:

 

08 0A 10 AC 02 1A 0B 50 62 53 65 72 69 61 6C 69 7A65

 

15118 差距不言而喻!下面分别分析一下这两段字节码的内容,先看java序列化的内容:

 

Java序列化一个对象产生的字节码是自描述型的,也就是说不借助其他的信息,仅仅从它本身的内容就能够找出这个对象的所有信息,比如说类元数据描述、类的属性、属性的值以及父类的所有信息。

Java序列化是将这些信息分成3个部分:

1.开头部分(颜色表示的都是常量,在java.io.ObjectStreamConstants 类中)

AC ED:写入流的幻数,STREAM_MAGIC;

00 05:写入流的版本号,STREAM_VERSION;

 

2.类描述部分(包括父类的描述信息)

73:TC_OBJECT, 声明这是一个新的对象;

72:TC_CLASSDESC,声明这里开始一个新Class;

00 18:class类名的长度(也就是”serialize.compare.UserDO”的长度);

73 65 72 69 61 6C 69 7A 65 2E 63 6F6D 70 61 72 65 2E 55 73 65 72 44 4F:这24个字节码转化成字符串就是:”serialize.compare.UserDO”;

5A A9 D2 C7 76 44 DDE3 serialVersionUID =6532984488602164707L,

(如果没有serialVersionUID会随机生成一个);

02:标记号,该值表示该对象支持序列化;

00 02:该类所包含属性的个数(idname);

49:字符“I”的值,代表属性的类型为Integer类型(参见ObjectStreamField);

00 02:属性名称的长度;(“id”.length()==2);

69 64:属性名称:id;

4C字符“L”的值,代表属性的类型为Object类型(参见ObjectStreamField);

00 04:属性名称的长度;(“name”.length()==4);

6E 61 6D 65:属性名称:”name”;

74TC_STRING,代表一个new String,这里是用来引用父类BaseUserDO;

00 12:对象签名的长度;

4C 6A 61 76 61 2F 6C 61 6E 67 2F 5374 72 69 6E 67 3BLjava/lang/String;

78:TC_ENDBLOCKDATA对象块结束的标志,7478之间的内容是用来说明UserDO BaseUserDO之间的继承关系的。

72TC_CLASSDESC,声明这里开始一个新Class;即父类BaseUserDO;

00 1Cclass类名的长度(也就是”serialize.compare.BaseUserDO”的长度);

73 65 72 69 61 6C 69 7A 65 2E 63 6F6D 70 61 72 65 2E 42 61 73 65 55 73 65 72 44 4F”serialize.compare.BaseUserDO”;

4F 17 51 92 BB 30 9554: serialVersionUID=5699113544108250452L;

02标记号,该值表示该对象支持序列化;

00 01该类所包含属性的个数(pid);

49字符“I”的值,代表属性的类型,也就是int;

00 03:属性名称的长度;

70 69 64:”pid”;

78: TC_ENDBLOCKDATA对象块结束的标志;

70TC_NULL,说明没有其他超类的标志;

 

3.属性值部分:(从父类开始将实例对象的实际值输出)

        0000 00 0A:10;(int pid =10;)

    0000 01 2C:300;(int id = 300;)

    74:TC_STRING;说明下面这个值的类型是String型的;

    000D:这个字符串的长度是13;

    4A61 76 61 53 65 72 69 61 6C 69 7A65:”JavaSerialize”;

从上面的解析可以看出序列化的内容大部分到在描述自己,而我们关心的值的部分即红颜色的部分只暂很小的一个部分,21个字节,占比13.91%。空间浪费严重。

 

再来看下ProtocalBuffer序列化,Pb在序列化之前需要定义一个.proto文件,用于描述一个message的数据结构,如下:

package tutorial;

 

option java_package ="serialize.compare";

option java_outer_classname ="UserAgent";

 

message UserDO{

    optionalint32 pid = 1;

    optionalint32 id = 2;

    optionalstring name = 3;

}

再使用PB的编译器编译这个.proto文件生成一个代理类,即UserAgent.java,对象的序列化和反序列化就通过这个代理类来实现;对象经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对,非常紧凑。如下图所示:


Java序列化与ProtocalBuffer序列化之深入分析(转)

Key:是由公式计算出来的:(field_number <<3) | wire_type,即Key的后三个比特为wire_type;

Value:是进过编码处理过的字节码;包括:Varintzigzag;

wire_type对应表:

Type

Meaning

Used For

0

Varint

int32,int64, uint32, uint64, sint32, sint64, bool, enum

1

64-bit

fixed64,sfixed64, double

2

Length-delimited

string,bytes, embedded messages, packed repeated fields

3

Start group

Groups (deprecated)

4

End group

Groups (deprecated)

5

32-bit

fixed32,sfixed32, float

 

Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。比如对于 int32 类型的数字,一般需要 个 byte 来表示。但是采用Varint,对于很小的 int32 类型的数字,则可以用 个 byte 来表示。当然凡事都有好的也有不好的一面,采用Varint 表示法,大的数字则需要 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。对于带符号的整数则是采用zigzag编码来处理,这样避免用一个很大的二进制数来表示一个负数,对应的wire_type是sint32,sint64.

 

了解了以上知识后就可以开解析一下以下序列化内容了:

08 0A 10 AC 02 1A 0B 50 62 53 65 72 69 61 6C 69 7A65 

08 0A:这是一个key-value对,08key,(1<<3)|0计算得出;
0Avalue,因为采用Varint编码10只需要一个字节来表示;java序列化中则是用4个字节来表示:00 00 00 0A
10 AC 02: key(2<<3)|0计算得出;value也是采用Varint编码;
演算一下:AC > 10*16+12 > 172 > 128+32+8+4 > 1010 1100
(高位是1说明还没有结束,下一个字节也是这个值的一部分)
02 > 0000 0010 (高位是0说明结束)
> 1010 1100   0000 0010
> 010 1100 000 0010(去掉高位,因为高位只是个标记位)
> 000 0010 010 1100(little-endian互换位置)
> 100101100 (二进制)
> 300 (十进制)
 
1A 0B 50 62 53 65 72 69 61 6C 69 7A 65
1Akey:(3<<3)|2 (stringwire_type=2参照上面的表格)
0B:表示这个string的长度为11
50 62 53 65 72 69 61 6C 69 7A 65:string的内容:”PbSerialize”;
 
可以看出这些字节码中没有任何类元素的描述,属性也是用tag来表示,
而且int32int64number型都采用了Varint编码,
我们的业务对象很多属性都是用int型的用来表示各种状态,而且值都是0,1,2,3之类的少于128的值,
那么这些value都只需用一个字节来存储,大大减少了空间。Value 占比也非常高:达到了77.78%。