`
wankunde
  • 浏览: 157881 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

Java序列化与ProtocalBuffer序列化之深入分析

 
阅读更多

转载:http://kangsg219.iteye.com/blog/904762

 

最近在做交易缓存化项目,要求缓存淘宝15天内的所有交易订单、支付订单和物流订单,数量在2-3亿;这就对空间和时间提出了很高的要求,因此研究并测试了几种序列化和压缩技术,下面是对Java序列化与ProtocalBuffer序列化对象生成的字节码的分析,通过分析能很明显地看出ProtocalBuffer序列化的优势所在。

 

从一个简单对象的序列化内容来看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 00 05 73 72 00 18 73 65 72 69 61 6C 69 7A 65 2E 63 6F 6D 70 61 72 65 2E 55 73 65 72 44 4F 5A A9 D2 C7 76 44 DD E3 02 00 02 49 00 02 69 64 4C 00 04 6E 61 6D 65 74 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 78 72 00 1C 73 65 72 69 61 6C 69 7A 65 2E 63 6F 6D 70 61 72 65 2E 42 61 73 65 55 73 65 72 44 4F 4F 17 51 92 BB 30 95 54 02 00 01 49 00 03 70 69 64 78 70 00 00 00 0A 00 00 01 2C 74 00 0D 4A 61 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 7A 65

 

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 6F 6D 70 61 72 65 2E 55 73 65 72 44 4F:这24个字节码转化成字符串就是:”serialize.compare.UserDO”;

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

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

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

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

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

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

69 64:属性名称:id;

4C字符“L”的值,代表属性的类型,不知道为什么用L来表示String

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

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

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

00 12:对象签名的长度;

4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B: Ljava/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 6F 6D 70 61 72 65 2E 42 61 73 65 55 73 65 72 44 4F”serialize.compare.BaseUserDO”;

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

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

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

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

00 03:属性名称的长度;

70 69 64:”pid”;

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

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

 

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

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

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

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

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

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

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

 

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

package tutorial;

 

option java_package = "serialize.compare";

option java_outer_classname = "UserAgent";

 

message UserDO{

    optional int32 pid = 1;

    optional int32 id = 2;

    optional string name = 3;

}

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


Key:是由公式计算出来的:(field_number << 3) | 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-delimi

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 类型的数字,一般需要 4 个 byte 来表示。但是采用Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。对于带符号的整数则是采用zigzag编码来处理,这样避免用一个很大的二进制数来表示一个负数,对应的wire_type是sint32,sint64.

 

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

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

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 (十进制)
 
1A0B 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%。
               

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics