用过dubbo的开发人员,在替换序列化时都会根据“经验”来选kryo为序列化框架,其原因是序列化协议非常高效,超过java原生序列化协议,hessian2协议,那kryo为什么高效呢?
序列化协议,所谓的高效,通常应该从两方面考虑:
- 序列化后的二进制序列大小。
- 序列化,反序列化的速率。
本节将重点探讨,kryo在减少序列化化二进制流上做的努力。
序列化:将各种数据类型(基本类型,包装类型,对象,数组,集合)等序列化为字节序列的过程。
反序列化:将字节转换为各种数据类型(基本类型,包装类型,对象,数组,集合)。
java中定义的数据类型所对应的序列化器在Kryo的构造函数中构造,其代码截图:
接下来将详细介绍java常用的数据类型的序列化机制,即Kryo是如何编码二进制流。
1,DefaultSerializers $ IntSerializer
int类型序列化
static public class IntSerializer extends Serializer {
{setImmutable(true);}public void write (Kryo kryo, Output output, Integer object) {output.writeInt(object, false);}public Integer read (Kryo kryo, Input input, Class type) {return input.readInt(false);}}
1.1整数---> byte [](序列化)
输出#writeInt
public int writeInt (int value, boolean optimizePositive) throws KryoException { // @1return writeVarInt(value, optimizePositive); // @2}
代码@ 1:布尔值optimizePositive,是否优化绝对值。如果optimizePositive:false,将对值进行移位运算,如果是正数,则存放的替换原值的变量,如果是负数的话,放置的位移绝对值的形式变量一,其算法为:value =(值<< 1)^(值>> 31),在反序列化时,通过该算法恢复原值:((结果>>> 1) ^-(result&1))。
代码@ 2:调用writeVarInt,采用变长编码来存储int而不是固定4字节。
输出#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) { // @2require(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序列化字节的编码规则。
代码@ 1:值>>> 7 == 0,一个数字,无符号右移(高位补0)7位后为0,说明该数字只占一个字节,并且高同轴必须为0,也就是该数字的范围在0-127(2 ^ 7 -1),对于字节的高位,低位的说明如下:
如果该值范围为0-127则使用1个字节存储int即可。在操作缓存区时buffer [position ++] =(byte)value,需要向输出的缓存区申请1个字节的空间,然后进行赋值,并返回本次申请的存储空间,对于require方法在Byte [],String序列化时重点讲解,包含缓存区的扩容,输出与输出流结合使用时的相关机制。
代码@ 2:值>>> 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是原第一个字节未存储的部分),此时缓冲区[1]的8位中的高位为0,表示存储结束。
下图展示了kryo用2个字节存储一个int类型的数据的示意图。
同理,用3个字节可以表示2 ^ 21 -1。kryo使用变长字节(1-5)个字节来存储int类型(java中固定占4字节)。
1.2 int反序列化(byte [] ---> int)
buffer [0] =低位,buffer [1]高位,具体解码实现为:Input#readVarInt,反序列化就是根据上述编码规则,将byte []序列化为int数字。
/** 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的存放是小端序列,越先读到的位,在整个i