kryo 各數據類型的序列化編碼機制(揭曉爲何高效的原理)

用過 dubbo 的開發人員,在選取序列化時都會根據「經驗」來選 kryo 爲序列化框架,其緣由是序列化協議很是高效,超過 java 原生序列化協議、hessian2 協議,那 kryo 爲何高效呢?java

序列化協議,所謂的高效,一般應該從兩方面考慮:算法

  1. 序列化後的二進制序列大小。
  2. 序列化、反序列化的速率。

> 本節將重點探討,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 &lt;&lt; 1) ^ (value &gt;&gt; 31);
		if (value &gt;&gt;&gt; 7 == 0) {                                           // @1 
			require(1);                                                    
			buffer[position++] = (byte)value;                  
			return 1;
		}
		if (value &gt;&gt;&gt; 14 == 0) {                                          // @2
			require(2);
			buffer[position++] = (byte)((value &amp; 0x7F) | 0x80);
			buffer[position++] = (byte)(value &gt;&gt;&gt; 7);
			return 2;
		}
		if (value &gt;&gt;&gt; 21 == 0) {
			require(3);
			buffer[position++] = (byte)((value &amp; 0x7F) | 0x80);
			buffer[position++] = (byte)(value &gt;&gt;&gt; 7 | 0x80);
			buffer[position++] = (byte)(value &gt;&gt;&gt; 14);
			return 3;
		}
		if (value &gt;&gt;&gt; 28 == 0) {
			require(4);
			buffer[position++] = (byte)((value &amp; 0x7F) | 0x80);
			buffer[position++] = (byte)(value &gt;&gt;&gt; 7 | 0x80);
			buffer[position++] = (byte)(value &gt;&gt;&gt; 14 | 0x80);
			buffer[position++] = (byte)(value &gt;&gt;&gt; 21);
			return 4;
		}
		require(5);
		buffer[position++] = (byte)((value &amp; 0x7F) | 0x80);
		buffer[position++] = (byte)(value &gt;&gt;&gt; 7 | 0x80);
		buffer[position++] = (byte)(value &gt;&gt;&gt; 14 | 0x80);
		buffer[position++] = (byte)(value &gt;&gt;&gt; 21 | 0x80);
		buffer[position++] = (byte)(value &gt;&gt;&gt; 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) &lt; 5) return readInt_slow(optimizePositive);
		int b = buffer[position++];
		int result = b &amp; 0x7F;
		if ((b &amp; 0x80) != 0) {
			byte[] buffer = this.buffer;
			b = buffer[position++];
			result |= (b &amp; 0x7F) &lt;&lt; 7;
			if ((b &amp; 0x80) != 0) {
				b = buffer[position++];
				result |= (b &amp; 0x7F) &lt;&lt; 14;
				if ((b &amp; 0x80) != 0) {
					b = buffer[position++];
					result |= (b &amp; 0x7F) &lt;&lt; 21;
					if ((b &amp; 0x80) != 0) {
						b = buffer[position++];
						result |= (b &amp; 0x7F) &lt;&lt; 28;
					}
				}
			}
		}
		return optimizePositive ? result : ((result &gt;&gt;&gt; 1) ^ -(result &amp; 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 &gt; 1 &amp;&amp; charCount &lt; 64) {  // @3
			ascii = true;
			for (int i = 0; i &lt; charCount; i++) {
				int c = value.charAt(i);
				if (c &gt; 127) {
					ascii = false;
					break;
				}
			}
		}
		if (ascii) {     // @4
			if (capacity - position &lt; 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 &gt;= charCount) {     // @6
				// Try to write 8 bit chars.
				byte[] buffer = this.buffer;
				int position = this.position;
				for (; charIndex &lt; charCount; charIndex++) {
					int c = value.charAt(charIndex);
					if (c &gt; 127) break;
					buffer[position++] = (byte)c;
				}
				this.position = position;
			}
			if (charIndex &lt; 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 &gt;&gt;&gt; 6 == 0) {
			require(1);
			buffer[position++] = (byte)(value | 0x80); // Set bit 8.
		} else if (value &gt;&gt;&gt; 13 == 0) {
			require(2);
			byte[] buffer = this.buffer;
			buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8.
			buffer[position++] = (byte)(value &gt;&gt;&gt; 6);
		} else if (value &gt;&gt;&gt; 20 == 0) {
			require(3);
			byte[] buffer = this.buffer;
			buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8.
			buffer[position++] = (byte)((value &gt;&gt;&gt; 6) | 0x80); // Set bit 8.
			buffer[position++] = (byte)(value &gt;&gt;&gt; 13);
		} else if (value &gt;&gt;&gt; 27 == 0) {
			require(4);
			byte[] buffer = this.buffer;
			buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8.
			buffer[position++] = (byte)((value &gt;&gt;&gt; 6) | 0x80); // Set bit 8.
			buffer[position++] = (byte)((value &gt;&gt;&gt; 13) | 0x80); // Set bit 8.
			buffer[position++] = (byte)(value &gt;&gt;&gt; 20);
		} else {
			require(5);
			byte[] buffer = this.buffer;
			buffer[position++] = (byte)(value | 0x40 | 0x80); // Set bit 7 and 8.
			buffer[position++] = (byte)((value &gt;&gt;&gt; 6) | 0x80); // Set bit 8.
			buffer[position++] = (byte)((value &gt;&gt;&gt; 13) | 0x80); // Set bit 8.
			buffer[position++] = (byte)((value &gt;&gt;&gt; 20) | 0x80); // Set bit 8.
			buffer[position++] = (byte)(value &gt;&gt;&gt; 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 &lt; charCount; charIndex++) {                                                                            // @1
			if (position == capacity) require(Math.min(, charCount - charIndex));                               // @2
			int c = value.charAt(charIndex);                                                                                        // @3
			if (c &lt;= 0x007F) {                                                                                                               // @4
				buffer[position++] = (byte)c;
			} else if (c &gt; 0x07FF) {                                                                                                       // @5
				buffer[position++] = (byte)(0xE0 | c &gt;&gt; 12 &amp; 0x0F);
				require(2);
				buffer[position++] = (byte)(0x80 | c &gt;&gt; 6 &amp; 0x3F);
				buffer[position++] = (byte)(0x80 | c &amp; 0x3F);
			} else {                                                                                                                                // @6
				buffer[position++] = (byte)(0xC0 | c &gt;&gt; 6 &amp; 0x1F);
				require(1);
				buffer[position++] = (byte)(0x80 | c &amp; 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 字符的長度。

  1. 若是是 null,則用 1 個字節表示,其二進制爲 1000 0000。
  2. 若是是""空字符串,則用 1 個字節表示,其二進制爲 1000 0001。
  3. 若是字符長度大於 1 且小於 64,而且字符全是 ascii 字符(小等於127),則每一個字符用一個字節表示,最後一個字節的高位置 1,表示 String字符的結束。【優化點,若是是 ascii 字符,編碼時不須要使用 length+內容的方式,而是直接寫入內容】
  4. 若是不知足上述條件,則須要使用 length + 內容的方式。
  1. 用一個變長int寫入字符的長度,每一字節,高兩位分別爲 編碼標記(1:utf8)、是否結束標記(1:否;0:結束)

    2)將內容用utf-8編碼寫入字節序列中,utf8,用變長字節(1-3)個字節表示一個字符(英文、中文)。每個字節,使用6爲,高兩位爲標誌位。【16位】

    1. 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 &gt;&gt;&gt; 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 &gt;&gt;&gt; 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 &gt;&gt; 24);
	buffer[position++] = (byte)(value &gt;&gt; 16);
	buffer[position++] = (byte)(value &gt;&gt; 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 &gt;&gt;&gt; 56);
	buffer[position++] = (byte)(value &gt;&gt;&gt; 48);
	buffer[position++] = (byte)(value &gt;&gt;&gt; 40);
	buffer[position++] = (byte)(value &gt;&gt;&gt; 32);
	buffer[position++] = (byte)(value &gt;&gt;&gt; 24);
	buffer[position++] = (byte)(value &gt;&gt;&gt; 16);
	buffer[position++] = (byte)(value &gt;&gt;&gt; 8);
	buffer[position++] = (byte)value;
}

BigInteger 序列化實現,總體格式與 String 類型同樣,由 length + 內容構成。

  1. 若是爲 null,則寫入一個字節,其值爲 0,表示長度爲 0。
  2. 若是爲 BigInteger.ZERO,則長度寫入 2,隨後再寫入 1 個字節的內容,字節內容爲 0,表示 ZERO。
  3. 將 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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實例的編碼規則:

  1. 若是爲 null,用變長 int,實際使用 1 個字節,存儲值爲 0。

  2. 若是該類經過類註冊機制註冊到 kryo 時,則序列化 (nameId + 2),用變長 int 存儲。

  3. 若是該類未經過類註冊機制註冊到 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 &lt; 0 || ordinal &gt; enumConstants.length - 1)
				throw new KryoException("Invalid ordinal for enum \"" + type.getName() + "\": " + ordinal);
			Object constant = enumConstants[ordinal];
			return (Enum)constant;
		}
	}

枚舉類型序列化(支持null):

  1. 若是爲null,則使用變長int,實際用一個字節存儲0。
  2. 若是不爲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 &lt; 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 對各類數據類型的序列化機制,其再下降序列化大小方面作了以下優化:-

  1. Kryo序列化的「對象」是數據以及少許元信息,這和 JAVA 默認的序列化的本質區別,java 默認的序列化的目的是語言層面的,將類、對象的全部信息都序列化了,也就是就算是不加載 class 的定義,也能根據序列化後的信息動態構建類的全部信息。而 Kryo反序列化時,必須能加載類的定義,這樣 Kryo 能節省大量的字節空間。

  2. 使用變長 int、變長 long 存儲 int、long 類型,大大節省空間。-

  3. 元數據(字符串類型)使用緩存機制,重複出現的字符串使用 int 來存儲,節省存儲空間。

  4. 字符串類型使用UTF-8存儲,但會使用ascii碼進一步優化空間。

下一篇將重點分析 Kryo 序列化的過程,其入口函數:Kryo#writeClassAndObject。

最後,親愛的讀者朋友們,以上就是本文的所有內容了,Kryo序列化爲何這麼高效是否已Get,歡迎留言討論。原創不易,莫要白票,請你爲本文點贊個吧,這將是我寫做更多優質文章的最強動力。


若是以爲文章對你有點幫助,請掃描以下二維碼,第一時間閱讀最新推文,回覆【源碼】,將得到成體系剖析JAVA系主流中間件的源碼分析專欄。 在這裏插入圖片描述 </enumset></enumset></enum></enum></string></string></integer></integer>

相關文章
相關標籤/搜索