用過 dubbo 的開發人員,在選取序列化時都會根據「經驗」來選 kryo 爲序列化框架,其緣由是序列化協議很是高效,超過 java 原生序列化協議、hessian2 協議,那 kryo 爲何高效呢?java
序列化協議,所謂的高效,一般應該從兩方面考慮:算法
- 序列化後的二進制序列大小。
- 序列化、反序列化的速率。
> 本節將重點探討,kryo在減小序列化化二進制流上作的努力。sql
序列化:將各類數據類型(基本類型、包裝類型、對象、數組、集合)等序列化爲 byte 數組的過程。數組
反序列化:將 byte 數組轉換爲各類數據類型(基本類型、包裝類型、對象、數組、集合)。緩存
java 中定義的數據類型所對應的序列化器 在Kryo 的構造函數中構造,其代碼截圖: 安全
接下來將詳細介紹java經常使用的數據類型的序列化機制,即Kryo是如何編碼二進制流。數據結構
一、DefaultSerializers$IntSerializer
int類型序列化框架
static public class IntSerializer extends Serializer<integer> { { setImmutable(true); } public void write (Kryo kryo, Output output, Integer object) { output.writeInt(object, false); } public Integer read (Kryo kryo, Input input, Class<integer> type) { return input.readInt(false); } }
1.1 Integer ---> byte[] (序列化)
Output#writeInt函數
public int writeInt (int value, boolean optimizePositive) throws KryoException { // @1 return writeVarInt(value, optimizePositive); // @2 }
代碼@1:boolean optimizePositive,是否優化絕對值。若是 optimizePositive: false,則會對value進行移位運算,若是是正數,則存放的值爲原值的兩倍,若是是負數的話,存放的值爲絕對值的兩倍減去一,其算法爲:value = (value << 1) ^ (value >> 31),在反序列化時,經過該算法恢復原值:((result >>> 1) ^ -(result & 1))。源碼分析
代碼@2:調用writeVarInt,採用變長編碼來存儲int而不是固定4字節。
Output#writeVarInt
public int writeVarInt (int value, boolean optimizePositive) throws KryoException { if (!optimizePositive) value = (value << 1) ^ (value >> 31); if (value >>> 7 == 0) { // @1 require(1); buffer[position++] = (byte)value; return 1; } if (value >>> 14 == 0) { // @2 require(2); buffer[position++] = (byte)((value & 0x7F) | 0x80); buffer[position++] = (byte)(value >>> 7); return 2; } if (value >>> 21 == 0) { require(3); buffer[position++] = (byte)((value & 0x7F) | 0x80); buffer[position++] = (byte)(value >>> 7 | 0x80); buffer[position++] = (byte)(value >>> 14); return 3; } if (value >>> 28 == 0) { require(4); buffer[position++] = (byte)((value & 0x7F) | 0x80); buffer[position++] = (byte)(value >>> 7 | 0x80); buffer[position++] = (byte)(value >>> 14 | 0x80); buffer[position++] = (byte)(value >>> 21); return 4; } require(5); buffer[position++] = (byte)((value & 0x7F) | 0x80); buffer[position++] = (byte)(value >>> 7 | 0x80); buffer[position++] = (byte)(value >>> 14 | 0x80); buffer[position++] = (byte)(value >>> 21 | 0x80); buffer[position++] = (byte)(value >>> 28); return 5; }
其思想是採起變長字節來存儲 int 類型的數據,int 在 java 是固定 4 字節,因爲在應用中,通常使用的 int 數據都不會很大,4 個字節中,存在高位字節全是存儲 0 的狀況,故 kryo 爲了減小在序列化流中的大小,儘可能按需分配,kryo 採用 1 - 5 個字節來存儲 int 數據,爲何 int 類型在 JAVA 中最多 4 個字節,爲何變長 int 可能須要 5 個字節才能存儲呢?這與變長字節須要標誌位有關,下文根據代碼來推測 kryo 關於 int 序列化 byte 數組的編碼規則。
代碼@1:value >>> 7 == 0 ,一個數字,無符號右移(高位補0) 7 位後爲 0,說明該數字只佔一個字節,而且高兩位必須爲 0,也就是該數字的範圍在 0-127(2^7 -1), 對於字節的高位,低位的說明以下:
若是該值範圍爲 0-127 則使用 1 個字節存儲 int 便可。在操做緩存區時 buffer[position++] = (byte)value,須要向 Output 的緩存區申請 1 個字節的空間,而後進行賦值,並返回本次申請的存儲空間,對於 require 方法在 Byte[]、String 序列化時重點講解,包含緩存區的擴容,Output 與輸出流結合使用時的相關機制。
代碼@2:value >>> 14 == 0,若是數字的範圍在 0 到 2^14-1 範圍之間,則須要兩個字節存儲,這裏爲何是 14,其主要緣由是,對於一個字節中的 8 位,kryo 須要將高位用來當標記位,用來 標識是否還須要讀取下一個字節。1:表示須要,0:表示不須要,也就是一個數據的結束。在變長 int 存儲過中,一個字節 8 位 kryo 可用來存儲數字有效位爲 7 位 。
舉例演示一下: kryo 兩字節能存儲的數據的特色是高字節中前兩位爲 0,例如: 0011 1011 0 010 1001 其存儲方式爲 buffer[0] = 先存儲最後字節的低 7 位,010 1001 ,而後第一位以前,加 1,表示還須要申請第二個字節來存儲。此時buffer[0] = 1010 1001 buffer[1] = 存儲 011 1011 0(這個0是原第一個字節未存儲的部分) ,此時buffer[1]的8位中的高位爲0,表示存儲結束。
下圖展現了kryo用2個字節存儲一個int類型的數據的示意圖。
同理,用3個字節能夠表示2^21 -1。 kryo使用變長字節(1-5)個字節來存儲int類型(java中固定佔4字節)。
1.2 int反序列化(byte[] ---> int)
反序列化就是根據上述編碼規則,將 byte[] 序列化爲 int 數字。 buffer[0] = 低位,buffer[1] 高位, 具體解碼實現爲:Input#readVarInt
/** Reads a 1-5 byte int. It is guaranteed that a varible length encoding will be used. */ public int readVarInt (boolean optimizePositive) throws KryoException { if (require(1) < 5) return readInt_slow(optimizePositive); int b = buffer[position++]; int result = b & 0x7F; if ((b & 0x80) != 0) { byte[] buffer = this.buffer; b = buffer[position++]; result |= (b & 0x7F) << 7; if ((b & 0x80) != 0) { b = buffer[position++]; result |= (b & 0x7F) << 14; if ((b & 0x80) != 0) { b = buffer[position++]; result |= (b & 0x7F) << 21; if ((b & 0x80) != 0) { b = buffer[position++]; result |= (b & 0x7F) << 28; } } } } return optimizePositive ? result : ((result >>> 1) ^ -(result & 1)); }
Input#require(count)返回的是緩存區剩餘字節數(可讀)。其實現思路是,一個一個字節的讀取,讀到第一個字節後,首先提取有效存儲位的數據,buffer[ 0 ] & 0x7F,而後判斷高位是否爲1,若是不爲1,直接返回,若是爲1,則繼續讀取第二位buffer[1],一樣首先提取有效數據位(低7位),而後對這數據向左移7位,在與buffer[0] 進行或運算。也就是,varint的存放是小端序列,越先讀到的位,在整個int序列中越靠近低位。
二、String序列化
其實現類 DefaultSerializers$StringSerializer。
static public class StringSerializer extends Serializer<string> { { setImmutable(true); setAcceptsNull(true); // @1 } public void write (Kryo kryo, Output output, String object) { output.writeString(object); } public String read (Kryo kryo, Input input, Class<string> type) { return input.readString(); } }
代碼@1:String 位不可變、容許爲空,也就是序列化時須要考慮 String s = null 的狀況。
2.1 序列化 (String ----> byte[])
Output#writeString
public void writeString (String value) throws KryoException { if (value == null) { // @1 writeByte(0x80); // 0 means null, bit 8 means UTF8. return; } int charCount = value.length(); if (charCount == 0) { // @2 writeByte(1 | 0x80); // 1 means empty string, bit 8 means UTF8. return; } // Detect ASCII. boolean ascii = false; if (charCount > 1 && charCount < 64) { // @3 ascii = true; for (int i = 0; i < charCount; i++) { int c = value.charAt(i); if (c > 127) { ascii = false; break; } } } if (ascii) { // @4 if (capacity - position < charCount) writeAscii_slow(value, charCount); else { value.getBytes(0, charCount, buffer, position); position += charCount; } buffer[position - 1] |= 0x80; } else { writeUtf8Length(charCount + 1); // @5 int charIndex = 0; if (capacity - position >= charCount) { // @6 // Try to write 8 bit chars. byte[] buffer = this.buffer; int position = this.position; for (; charIndex < charCount; charIndex++) { int c = value.charAt(charIndex); if (c > 127) break; buffer[position++] = (byte)c; } this.position = position; } if (charIndex < charCount) writeString_slow(value, charCount, charIndex); // @7 } }
首先對字符串編碼成字節序列,一般採用的編碼方式爲 length:具體內容,一般的作法,表示字符串序列長度爲固定字節,例如 4 位,那 kryo 是如何來表示的呢?請看下文分析。
代碼@1:若是字符串爲 null,採用一個字節來表示長度,長度爲 0,而且該字節的高位填充 1,表示字符串使用 UTF-8 編碼,null 字符串的最終表示爲:1000 0000。
代碼@2:空字符串表示,長度用 1 來表示,一樣高位使用 1 填充表示字符串使用 UTF-8 編碼,空字符串最終表示爲:1000 0001。注:長度爲 1 表示空字符串。
代碼@3:若是字符長度大於 1 而且小於 64,依次檢查字符,若是其 ascii 小於 127,則認爲能夠用 ascii 來表示單個字符,不能超過 127 的緣由是,其中字節的高一位須要表示編碼,0 表示 ascii,當用 ascii 編碼來表示字符串是,第高 2 位須要用來表示是否結束標記。
代碼@4:若是使用 ascii 編碼,則單個字符,使用一個字節表示,高 1 位表示編碼標記爲,高 2 位表示是否結束標記。
代碼@5:按照 UTF-8 編碼,寫入其長度,用變長 int(varint) 寫入字符串長度,具體實現以下:
Output#writeUtf8Length
private void writeUtf8Length (int value) { if (value >>> 6 == 0) { require(1); buffer[position++] = (byte)(value | 0x80); // Set bit 8. } else if (value >>> 13 == 0) { require(2); byte[] buffer = this.buffer; buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8. buffer[position++] = (byte)(value >>> 6); } else if (value >>> 20 == 0) { require(3); byte[] buffer = this.buffer; buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8. buffer[position++] = (byte)((value >>> 6) | 0x80); // Set bit 8. buffer[position++] = (byte)(value >>> 13); } else if (value >>> 27 == 0) { require(4); byte[] buffer = this.buffer; buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8. buffer[position++] = (byte)((value >>> 6) | 0x80); // Set bit 8. buffer[position++] = (byte)((value >>> 13) | 0x80); // Set bit 8. buffer[position++] = (byte)(value >>> 20); } else { require(5); byte[] buffer = this.buffer; buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8. buffer[position++] = (byte)((value >>> 6) | 0x80); // Set bit 8. buffer[position++] = (byte)((value >>> 13) | 0x80); // Set bit 8. buffer[position++] = (byte)((value >>> 20) | 0x80); // Set bit 8. buffer[position++] = (byte)(value >>> 27); } }
用來表示字符串長度的編碼規則(int),第 8 位(高位)表示字符串的編碼,第 7 位(高位)表示是否還須要讀取下一個字節,也就是結束標記,1 表示未結束,0 表示結束。一個字節共 8 位,只有低 6 位用來存放數據,varint 採起的是小端序列。
代碼@6:若是當前緩存區有足夠的空間,先嚐試將字符串中單字節數據寫入到 buffer 中,碰到第一個非單字節字符時,結束。
代碼@7:將剩餘空間寫入緩存區,其實現方法:Output#writeString_slow(value, charCount, charIndex)
Output#writeString_slow
private void writeString_slow (CharSequence value, int charCount, int charIndex) { for (; charIndex < charCount; charIndex++) { // @1 if (position == capacity) require(Math.min(, charCount - charIndex)); // @2 int c = value.charAt(charIndex); // @3 if (c <= 0x007F) { // @4 buffer[position++] = (byte)c; } else if (c > 0x07FF) { // @5 buffer[position++] = (byte)(0xE0 | c >> 12 & 0x0F); require(2); buffer[position++] = (byte)(0x80 | c >> 6 & 0x3F); buffer[position++] = (byte)(0x80 | c & 0x3F); } else { // @6 buffer[position++] = (byte)(0xC0 | c >> 6 & 0x1F); require(1); buffer[position++] = (byte)(0x80 | c & 0x3F); } } }
代碼@1:循環遍歷字符的字符。
代碼@2:若是當前緩存區已經寫滿,嘗試申請(capacity 與 charCount - charIndex )的最小值,這裏無需擔憂字符不是單字節申請 charCount - charIndex 空間不足的問題,後面咱們會詳細分析 require 方法,字節不夠時會觸發緩存區擴容或刷寫到流中,再重複利用緩存區。
代碼@3:int c = value.charAt(charIndex); 將字符類型轉換爲 int 類型,一箇中文字符對應一個 int 數字,這是由於 java 使用 unicode 編碼,每一個字符佔用 2 個字節,char 向 int 類型轉換,就是將 2 字節的字節編碼,轉換成對應的二進制,而後用 10 進製表示的數字。
代碼@4:若是值小於等 0x7F(127),直接存儲在 1 個字節中,此時高位 4 個字節的範圍在(0-7).]。
代碼@5:若是值大於 0x07FF(二進制 0000 0111 1111 1111),第一個大於 0x7F 的值爲(0000 1000 0000 0000), 即 2^12,數據有效位至少 12 位,使用 3 字節來存儲,具體存儲方式爲:
1)buffer[0] :buffer[position++] = (byte)(0xE0 | c >> 12 & 0x0F); 首先將 c 右移 12 位再與 0x0F 進行與操做,其意義就是先提取 c 的第 16-13(4位的值),並與 0xE0 取或,最終的值爲 0xE (16-13) 位的值,從 Input 讀取字符串能夠看出,是根據 0xE0 做爲存儲該字符須要 3 個字節的依據,而且只取 16-13 位的值做爲其高位的有效位,也就是說字符編碼的值,不會超過 0XFFFF,也就是兩個字節(正好與java unicode編碼吻合)。
2)buffer[1]:存儲第 12-7(共6位),c >> 6 & 0x3F,而後與 0X80 進行或,高位設置爲 1,表示 UTF-8 編碼,其實再反序列化時,這個高位設置爲 1,未有實際做用。
3)buffer[2]:存儲第 6-1(共6位),0x80 | c & 0x3F,一樣高位置 1。
2.2 字符串反序列化 (byte[] ----> String)
在講解反序列化時,總結一下String序列化的編碼規則
String 序列化規則:String 序列化的總體結構爲 length + 內容,注意,這裏的 length 不是內容字節的長度,而是 String 字符的長度。
- 若是是 null,則用 1 個字節表示,其二進制爲 1000 0000。
- 若是是""空字符串,則用 1 個字節表示,其二進制爲 1000 0001。
- 若是字符長度大於 1 且小於 64,而且字符全是 ascii 字符(小等於127),則每一個字符用一個字節表示,最後一個字節的高位置 1,表示 String字符的結束。【優化點,若是是 ascii 字符,編碼時不須要使用 length+內容的方式,而是直接寫入內容】
- 若是不知足上述條件,則須要使用 length + 內容的方式。
-
用一個變長int寫入字符的長度,每一字節,高兩位分別爲 編碼標記(1:utf8)、是否結束標記(1:否;0:結束)
2)將內容用utf-8編碼寫入字節序列中,utf8,用變長字節(1-3)個字節表示一個字符(英文、中文)。每個字節,使用6爲,高兩位爲標誌位。【16位】
- 3字節的存儲爲 【4位】 + 【6位】 + 【6位】,根據第一個字節高4位判斷得出 須要幾個字節來存儲一個字符。
其反序列化的入口爲 Input#readString,就是按照上述規則進行解析便可,就不深刻探討了,有興趣的話,能夠本身去指定地方查閱。
三、boolean類型序列化
實現類爲 DefaultSerializers$BooleanSerializer,序列化:使用 1 個字節存儲 boolean 類型,若是爲 true,則寫入 1,不然寫入 0。
四、byte類型序列化
實現類爲:DefaultSerializers$ByteSerializer,序列化:直接將 byte 寫入字節流中便可。
五、char類型序列化
實現類爲:DefaultSerializers$CharSerializer
Output#writeChar
/** Writes a 2 byte char. Uses BIG_ENDIAN byte order. */ public void writeChar (char value) throws KryoException { require(2); buffer[position++] = (byte)(value >>> 8); buffer[position++] = (byte)value; }
序列化:char 在 java 中使用 2 字節存儲(unicode), kryo 在序列化時,按大端字節的順序,將 char 寫入字節流
六、short類型序列化
實現類爲 DefaultSerializers$ShortSerializer Output#writeShort
/** Writes a 2 byte short. Uses BIG_ENDIAN byte order. */ public void writeShort (int value) throws KryoException { require(2); buffer[position++] = (byte)(value >>> 8); buffer[position++] = (byte)value; }
序列化:與char類型序列化同樣,採用大端字節順序存儲。
七、long類型序列化
實現類爲:DefaultSerializers$LongSerializer
Output#writeLong
public int writeLong (long value, boolean optimizePositive) throws KryoException { return writeVarLong(value, optimizePositive); }
序列化:採起變長字節(1-9)位來存儲 long,其編碼規則與 int 變長類型一致,每一個字節的高位用來表示是否結束,1:表示還須要繼續讀取下一個字節,0:表示結束。
八、float類型序列化
實現類爲:DefaultSerializers$FloatSerializer
/** Writes a 4 byte float. */ public void writeFloat (float value) throws KryoException { writeInt(Float.floatToIntBits(value)); } /** Writes a 4 byte int. Uses BIG_ENDIAN byte order. */ public void writeInt (int value) throws KryoException { require(4); byte[] buffer = this.buffer; buffer[position++] = (byte)(value >> 24); buffer[position++] = (byte)(value >> 16); buffer[position++] = (byte)(value >> 8); buffer[position++] = (byte)value; }
序列化:首先將 float 按照 IEEE 754 編碼標準,轉換爲 int 類型,而後按大端序列,使用固定長度 4 字節來存儲 float,這裏之因此不使用變長字節來存儲 float,是由於使用 Float.floatToIntBits(value) 產生的值,比較大,基本都須要使用 4 字才能存儲,若是使用變長字節,則須要 5 字節,反而消耗的存儲空間更大。
九、DefaultSerializers$DoubleSerializer
Output#writeDouble 序列化:首先將 Double 按照 IEEE 754 編碼標準轉換爲 Long,而後纔去固定 8 字節存儲。 到目前爲止,介紹了8種基本類型(boolean、byte、char、short、int、float、long、double)和 String 類型的序列化與反序列化。
十、BigInteger序列化實現類爲:DefaultSerializers$BigIntegerSerializer
/** Writes an 8 byte double. */ public void writeDouble (double value) throws KryoException { writeLong(Double.doubleToLongBits(value)); } /** Writes an 8 byte long. Uses BIG_ENDIAN byte order. */ public void writeLong (long value) throws KryoException { require(8); byte[] buffer = this.buffer; buffer[position++] = (byte)(value >>> 56); buffer[position++] = (byte)(value >>> 48); buffer[position++] = (byte)(value >>> 40); buffer[position++] = (byte)(value >>> 32); buffer[position++] = (byte)(value >>> 24); buffer[position++] = (byte)(value >>> 16); buffer[position++] = (byte)(value >>> 8); buffer[position++] = (byte)value; }
BigInteger 序列化實現,總體格式與 String 類型同樣,由 length + 內容構成。
- 若是爲 null,則寫入一個字節,其值爲 0,表示長度爲 0。
- 若是爲 BigInteger.ZERO,則長度寫入 2,隨後再寫入 1 個字節的內容,字節內容爲 0,表示 ZERO。
- 將 BigInteger 轉換成 byte[] 數組,首先寫入長度 =( byte數組長度 + 1),而後寫入 byte 數組的內容便可。
十一、BigDecimal序列化
實現類爲:DefaultSerializers$BigDecimalSerializer
BigDecimal 的序列化與 BigInteger 同樣,首先是經過 BigDecimal#unscaledValue 方法返回對應的 BigInteger,而後序列化,在反序列化時經過 BigInteger 建立對應的 BigDecimal 便可。
十二、Class實例序列化
實現類爲:DefaultSerializers$ClassSerializer
public void write (Kryo kryo, Output output, Class object) { kryo.writeClass(output, object); // @1 output.writeByte((object != null && object.isPrimitive()) ? 1 : 0); // @2 }
代碼@1:調用 Kryo 的 writeClass 方法序列化 Class 實例。 代碼@2:寫入是不是包裝類型(針對8種基本類型)。
接下來咱們重點分析Kryo#writeClass
public Registration writeClass (Output output, Class type) { if (output == null) throw new IllegalArgumentException("output cannot be null."); try { return classResolver.writeClass(output, type); // @1 } finally { if (depth == 0 && autoReset) reset(); // @2 } }
代碼@1:首先調用 ClassResolver.wreteClass 方法。 代碼@2:完成一次寫入後,須要重置 Kryo 中的臨時數據結構,這也就是 kryo 實例非線程安全的緣由,其中幾個重要的數據結構會再 ClassResolver.writeClass 中詳細說明。
DefaultClassResolver#writeClass
public Registration writeClass (Output output, Class type) { if (type == null) { // @1 if (TRACE || (DEBUG && kryo.getDepth() == 1)) log("Write", null); output.writeVarInt(Kryo.NULL, true); return null; } Registration registration = kryo.getRegistration(type); // @2 if (registration.getId() == NAME) // @3 writeName(output, type, registration); else { if (TRACE) trace("kryo", "Write class " + registration.getId() + ": " + className(type)); output.writeVarInt(registration.getId() + 2, true); // @4 } return registration; }
代碼@1:若是 type 爲 null,則存儲 Kryo.NULL(0),使用變長 int 來存儲,0 在變長 int 中佔用 1 個字節。
代碼@2:根據 type 從 kryo 獲取類註冊信息,若是有調用 kryo#public Registration register (Class type)方法,則會返回其註冊關係。
代碼@3:若是不存在註冊關係,則須要將類型的全名寫入。
代碼@4:若是存在註冊關係,則 registration.getId() 將不等於 Kryo.NAME(-1),則將(registration.getId() + 2)使用變長 int 寫入字節流便可。
從這裏看出,若是將類預先註冊到 kryo 中,序列化字節流將變的更小,所謂的 kryo 類註冊機制就是將字符串的類全路徑名替換爲數字,但數字的分配與註冊順序相關,全部,若是要使用類註冊機制,必須在 kryo 對象建立時首先註冊,確保註冊順序一致。
接下來重點分析一下 writeName 方法
DefaultClassResolver#writeName
protected void writeName (Output output, Class type, Registration registration) { output.writeVarInt(NAME + 2, true); // @1 if (classToNameId != null) { // @2 int nameId = classToNameId.get(type, -1); / if (nameId != -1) { // if (TRACE) trace("kryo", "Write class name reference " + nameId + ": " + className(type)); output.writeVarInt(nameId, true); return; } } // Only write the class name the first time encountered in object graph. if (TRACE) trace("kryo", "Write class name: " + className(type)); int nameId = nextNameId++; // @3 if (classToNameId == null) classToNameId = new IdentityObjectIntMap(); // @4 classToNameId.put(type, nameId); // @5 output.writeVarInt(nameId, true); // @6 output.writeString(type.getName()); // @7 }
代碼@1:因爲是要寫入類的全路徑名,故首先使用變長 int 編碼寫入一個標記,表示是存儲的類名,而不是一個 ID。其標誌位爲 NAME + 2 = 1。存儲 0 表示 null。
代碼@2:若是 classToNameId 不爲空(IdentityObjectIntMap< Class>),根據 type 獲取 nameId,若是不爲空而且從緩存中能獲取到 nameId,則直接寫入 nameId,而不是寫入類名,這裏指在一次序列化過程當中,同一個類名例如(cn.uce.test.Test)只寫入一次,其餘級聯(重複)出現時,爲其分配一個 ID,進行緩存,具體能夠從下面的代碼中得知其意圖。
代碼@3:首先分配一全局遞增的 nameId。
代碼@4:若是 classToNameId 爲空,則建立一個實例。
代碼@5:將 type 與 nameId 進行緩存。
代碼@6:寫入 nameId。 代碼@7:寫入 type 的全路徑名。
注意 Kryo#writeClass,一次序列化 Class 實例後會調用 reset 方法,最終會清除本次 classToNameId ,classToNameId 並不能作一個全據的緩存的主要緣由是,在不一樣的 JVM 虛擬機中,同一個class type 對應的 nameId 不必定相同,故沒法實現共存,只能是做爲一個優化,在一次類序列化中,若是存在同一個類型,則第一個寫入類全路徑名,後面出現的則使用 id(int) 來存儲,節省空間。
爲了加深上述理解,咱們再來看一下 Class 實例的反序列化:
DefaultClassResolver#readClass
public Registration readClass (Input input) { int classID = input.readVarInt(true); // @1 switch (classID) { case Kryo.NULL: // @2 if (TRACE || (DEBUG && kryo.getDepth() == 1)) log("Read", null); return null; case NAME + 2: // Offset for NAME and NULL. // @3 return readName(input); } if (classID == memoizedClassId) return memoizedClassIdValue; Registration registration = idToRegistration.get(classID - 2); if (registration == null) throw new KryoException("Encountered unregistered class ID: " + (classID - 2)); if (TRACE) trace("kryo", "Read class " + (classID - 2) + ": " + className(registration.getType())); memoizedClassId = classID; memoizedClassIdValue = registration; return registration; }
代碼@1:首先讀取一個變長 int。
代碼@2:若是爲 Kryo.NULL 表示爲 null,直接返回 null 便可。
代碼@3:若是爲NAME + 2 則表示爲存儲的是類的全路徑名,則調用 readName 解析類的名字。
代碼@4:若是不爲上述值,說明存儲的是類型對應的ID值,也就是使用了類註冊機制。 之因此 idToRegistration.get(classID - 2),是由於在存儲時就是 nameId + 2。由於,0(表明null),1:表明按類全路徑名存儲,nameId 是從 3 開始存儲。 接下來再重點看一下 readName 的實現:
DefaultClassResolver#readName
protected Registration readName (Input input) { int nameId = input.readVarInt(true); if (nameIdToClass == null) nameIdToClass = new IntMap(); Class type = nameIdToClass.get(nameId); if (type == null) { // Only read the class name the first time encountered in object graph. String className = input.readString(); type = getTypeByName(className); if (type == null) { try { type = Class.forName(className, false, kryo.getClassLoader()); } catch (ClassNotFoundException ex) { if (WARN) warn("kryo", "Unable to load class " + className + " with kryo's ClassLoader. Retrying with current.."); try { type = Class.forName(className); } catch (ClassNotFoundException e) { throw new KryoException("Unable to find class: " + className, ex); } } if (nameToClass == null) nameToClass = new ObjectMap(); nameToClass.put(className, type); } nameIdToClass.put(nameId, type); if (TRACE) trace("kryo", "Read class name: " + className); } else { if (TRACE) trace("kryo", "Read class name reference " + nameId + ": " + className(type)); } return kryo.getRegistration(type); }
首先讀取類的 id,由於在序列化類時,若是序列化字符串時,首先先用變長 int 存儲類型的 nameId,而後再序列化類的全路徑名,這樣在一次反序列化時,第一次序列化時,將全列的全路徑使用 Class.forName 實例化對象後,而後存儲在局部方法緩存中(IntMap)中,在這一次序列化時再碰到同類型時,則根據 id 則能夠找到對象。
Class實例序列化總結:
Class實例序列化需求:序列化類的全路徑名,反序列化時根據 Class.forName 生成對應的實例。
kryo序列化Class實例的編碼規則:
-
若是爲 null,用變長 int,實際使用 1 個字節,存儲值爲 0。
-
若是該類經過類註冊機制註冊到 kryo 時,則序列化 (nameId + 2),用變長 int 存儲。
-
若是該類未經過類註冊機制註冊到 kryo,在一次序列化過程當中(包含級聯)時,類型第一次出現時,會分配一個 nameId,將 nameId + type 全路徑序列化,後續再出現該類型,則只序列化 nameId 便可。
1三、DefaultSerializers$DateSerializer
java.Util.Date、java.sql.Date等序列化時,只需序列化 Date#getTime() 返回的 long 類型,反序列化時根據 long 類型建立對應的實例便可。long 類型的編碼使用變長 long 格式進行序列化。
1四、枚舉類型Enum序列化
實現類爲:DefaultSerializers$EnumSerializer
static public class EnumSerializer extends Serializer<enum> { { setImmutable(true); setAcceptsNull(true); } private Object[] enumConstants; public EnumSerializer (Class<!--? extends Enum--> type) { enumConstants = type.getEnumConstants(); if (enumConstants == null) throw new IllegalArgumentException("The type must be an enum: " + type); } public void write (Kryo kryo, Output output, Enum object) { if (object == null) { output.writeVarInt(NULL, true); return; } output.writeVarInt(object.ordinal() + 1, true); } public Enum read (Kryo kryo, Input input, Class<enum> type) { int ordinal = input.readVarInt(true); if (ordinal == NULL) return null; ordinal--; if (ordinal < 0 || ordinal > enumConstants.length - 1) throw new KryoException("Invalid ordinal for enum \"" + type.getName() + "\": " + ordinal); Object constant = enumConstants[ordinal]; return (Enum)constant; } }
枚舉類型序列化(支持null):
- 若是爲null,則使用變長int,實際用一個字節存儲0。
- 若是不爲null,使用變長int,存儲object.ordinal()+1,也就是序列化該值在枚舉類型常量數組中的下標,因爲0表明爲空,則下標從1開始。
在反序列化時,經過Enum.class.getEnumConstants()獲取枚舉類型的常量數組,而後從二進制流中獲取下標便可。-
1五、EnumSet 類型序列化
實現類爲:DefaultSerializers$EnumSetSerializer
static public class EnumSetSerializer extends Serializer<enumset> { public void write (Kryo kryo, Output output, EnumSet object) { Serializer serializer; if (object.isEmpty()) { // @1 EnumSet tmp = EnumSet.complementOf(object); // @2 if (tmp.isEmpty()) throw new KryoException("An EnumSet must have a defined Enum to be serialized."); serializer = kryo.writeClass(output, tmp.iterator().next().getClass()).getSerializer(); // @3 } else { serializer = kryo.writeClass(output, object.iterator().next().getClass()).getSerializer(); } output.writeInt(object.size(), true); // @4 for (Object element : object) // @5 serializer.write(kryo, output, element); } public EnumSet read (Kryo kryo, Input input, Class<enumset> type) { Registration registration = kryo.readClass(input); EnumSet object = EnumSet.noneOf(registration.getType()); Serializer serializer = registration.getSerializer(); int length = input.readInt(true); for (int i = 0; i < length; i++) object.add(serializer.read(kryo, input, null)); return object; } public EnumSet copy (Kryo kryo, EnumSet original) { return EnumSet.copyOf(original); } }
EnumSet 是一個專爲枚舉設計的集合類,EnumSet 中的全部元素都必須是指定枚舉類型的枚舉值。在序列化 EnumSet 時,須要將 EnumSet 中存儲的枚舉類型進行序列化,而後再序列每個枚舉值。
序列化過程:
代碼@1:若是序列化的 EnumSet 爲空,則經過代碼 EnumSet.complementOf 方法建立一個其元素類型與指定 EnumSet 裏元素類型相同的 EnumSet 集合,新 EnumSet 集合包含原 EnumSet 集合所不包含的、此類枚舉類剩下的枚舉值(即新 EnumSet 集合和原 EnumSet 集合的集合元素加起來是該枚舉類的全部枚舉值)。-
代碼@3:首先序列化EnumSet中的枚舉類型Class實例,並獲取枚舉類型對應的序列器。
代碼@4:序列化EnumSet中元素的個數。
代碼@5:逐一序列化EnumSet中元素(一個個枚舉值)。
1六、StringBuffer序列化
實現類爲DefaultSerializers$StringBufferSerializer,序列化:與 String 序列化一致。
1七、StringBuilder序列化
實現類爲DefaultSerializers$StringBuilderSerializer,序列化:與 String 序列化一致。
1八、TreeMap序列化
實現類爲:DefaultSerializers$TreeMapSerializer
static public class TreeMapSerializer extends MapSerializer { public void write (Kryo kryo, Output output, Map map) { TreeMap treeMap = (TreeMap)map; kryo.writeClassAndObject(output, treeMap.comparator()); super.write(kryo, output, map); } // ...省略部分代碼 }
TreeMap的序列,首先,先序列化 TreeMap 的比較器,而後再序列化 TreeMap 中的數據。
序列化數據請看 MapSerializer MapSerializer#write
public void write (Kryo kryo, Output output, Map map) { int length = map.size(); output.writeInt(length, true); Serializer keySerializer = this.keySerializer; if (keyGenericType != null) { if (keySerializer == null) keySerializer = kryo.getSerializer(keyGenericType); keyGenericType = null; } Serializer valueSerializer = this.valueSerializer; if (valueGenericType != null) { if (valueSerializer == null) valueSerializer = kryo.getSerializer(valueGenericType); valueGenericType = null; } for (Iterator iter = map.entrySet().iterator(); iter.hasNext();) { Entry entry = (Entry)iter.next(); if (keySerializer != null) { if (keysCanBeNull) kryo.writeObjectOrNull(output, entry.getKey(), keySerializer); else kryo.writeObject(output, entry.getKey(), keySerializer); } else kryo.writeClassAndObject(output, entry.getKey()); if (valueSerializer != null) { if (valuesCanBeNull) kryo.writeObjectOrNull(output, entry.getValue(), valueSerializer); else kryo.writeObject(output, entry.getValue(), valueSerializer); } else kryo.writeClassAndObject(output, entry.getValue()); } }
其序列化方法就是遍歷 Map 中的元素,調用 Kryo#writeClassAndObject 進行序列化,Kryo#writeClassAndObject 涉及到 Kryo 整個序列化流程,將在下節介紹。
本節就講述到這裏了,本節詳細分析了 Kryo 對各類數據類型的序列化機制,其再下降序列化大小方面作了以下優化:-
-
Kryo序列化的「對象」是數據以及少許元信息,這和 JAVA 默認的序列化的本質區別,java 默認的序列化的目的是語言層面的,將類、對象的全部信息都序列化了,也就是就算是不加載 class 的定義,也能根據序列化後的信息動態構建類的全部信息。而 Kryo反序列化時,必須能加載類的定義,這樣 Kryo 能節省大量的字節空間。
-
使用變長 int、變長 long 存儲 int、long 類型,大大節省空間。-
-
元數據(字符串類型)使用緩存機制,重複出現的字符串使用 int 來存儲,節省存儲空間。
-
字符串類型使用UTF-8存儲,但會使用ascii碼進一步優化空間。
下一篇將重點分析 Kryo 序列化的過程,其入口函數:Kryo#writeClassAndObject。
最後,親愛的讀者朋友們,以上就是本文的所有內容了,Kryo序列化爲何這麼高效是否已Get,歡迎留言討論。原創不易,莫要白票,請你爲本文點贊個吧,這將是我寫做更多優質文章的最強動力。
若是以爲文章對你有點幫助,請掃描以下二維碼,第一時間閱讀最新推文,回覆【源碼】,將得到成體系剖析JAVA系主流中間件的源碼分析專欄。 </enumset></enumset></enum></enum></string></string></integer></integer>