1. 程式人生 > >Java序列化與ProtocalBuffer序列化之深入分析(轉)

Java序列化與ProtocalBuffer序列化之深入分析(轉)

今天看了《Java序列化與ProtocalBuffer序列化之深入分析》,感覺有所收穫。原文中對ObjectStreamField中關於屬性型別與字元表示的對映沒有指出來,在原帖中回覆了作者,這裡稍作修改並轉發

從一個簡單物件的序列化內容來看java序列化與ProtocalBuffer序列化機制的不同之處以及優劣所在。物件準備如下:

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

package serialize.compare;

publicclass BaseUserDO implements Serializable{

privatestatic

finallongserialVersionUID =5699113544108250452L;

privateintpid;

}

子類UserDO.java繼承上面的父類

package serialize.compare;

publicclass UserDO extends BaseUserDO{

privatestaticfinallongserialVersionUID =6532984488602164707L;

privateintid;

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 0A10 AC 021A0B 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 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 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 對,非常緊湊。如下圖所示:


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 型別的數字,一般需要 4 個 byte 來表示。但是採用Varint,對於很小的 int32 型別的數字,則可以用 1 個 byte 來表示。當然凡事都有好的也有不好的一面,採用Varint 表示法,大的數字則需要 5 個 byte 來表示。從統計的角度來說,一般不會所有的訊息中的數字都是大數,因此大多數情況下,採用 Varint 後,可以用更少的位元組數來表示數字資訊。對於帶符號的整數則是採用zigzag編碼來處理,這樣避免用一個很大的二進位制數來表示一個負數,對應的wire_type是sint32,sint64.

瞭解了以上知識後就可以開解析一下以下序列化內容了:

08 0A10 AC 021A0B 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 (十進位制)
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%。