用过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的存放是小端序列,越先读到的位,在整个int序列中越靠近低位。
2,字符串序列化
其实现类DefaultSerializers $ StringSerializer。
static public class StringSerializer extends Serializer {{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 type) {return input.readString();}}
代码@ 1:String位不可变,允许为空,也就是序列化时需要考虑String s = null的情况。
2.1序列化(字符串----> byte [])
输出#writeString
public void writeString (String value) throws KryoException {if (value == null) { // @1writeByte(0x80); // 0 means null, bit 8 means UTF8.return;}int charCount = value.length(); if (charCount == 0) { // @2writeByte(1 | 0x80); // 1 means empty string, bit 8 means UTF8.return;}// Detect ASCII.boolean ascii = false;if (charCount > 1 && charCount < 64) { // @3ascii = true;for (int i = 0; i < charCount; i++) {int c = value.charAt(i);if (c > 127) {ascii = false;break;}}}if (ascii) { // @4if (capacity - position < charCount)writeAscii_slow(value, charCount);else {value.getBytes(0, charCount, buffer, position);position += charCount;}buffer[position - 1] |= 0x80;} else {writeUtf8Length(charCount + 1); // @5int 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}}
首先对字符串编码成字节序列,通常采用的编码方式为长度:具体内容,通常的做法,表示串行序列长度为固定字节,例如4位,那kryo是如何来表示的呢?请看初步分析。
代码@ 1:如果字符串为null,采用一个字节来表示长度,长度为0,并且该字节的高位填充1,表示字符串使用UTF-8编码,空字符串的最终表示为:1000 0000 。
代码@ 2:空字符串表示,长度用1来表示,同样高位使用1填充表示字符串使用UTF-8编码,空字符串最终表示为:10000001。注:长度为1表示空字符串。
代码@ 3:如果字符长度大于1并且小于64,依次检查字符,如果其ascii小于127,则认为可以使用ascii来表示空格,不能超过127的原因是,其中字节的高一个需要表示编码,0表示ascii,当用ascii编码来表示字符串是,第高2位需要使用表示还是结束标记。
代码@ 4:如果使用ascii编码,则单独字符,使用一个字节表示,高1位表示编码标记为,高2位表示是否结束标记。
代码@ 5:按照UTF-8编码,写入其长度,用变长int(varint)写入字符串长度,具体实现如下:
输出#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:如果当前缓存区有足够的空间,先尝试将字符串中单字节数据写入到缓冲区中,碰到第一个非单字节字符时,结束。
代码@ 7:将剩余空间写入缓存区,其实现方法:Output#writeString_slow(value,charCount,charIndex)
输出#writeString_slow
private void writeString_slow (CharSequence value, int charCount, int charIndex) {for (; charIndex < charCount; charIndex++) { // @1if (position == capacity) require(Math.min(, charCount - charIndex)); // @2int c = value.charAt(charIndex); // @3if (c <= 0x007F) { // @4buffer[position++] = (byte)c;} else if (c > 0x07FF) { // @5buffer[position++] = (byte)(0xE0 | c >> 12 & 0x0F);require(2);buffer[position++] = (byte)(0x80 | c >> 6 & 0x3F);buffer[position++] = (byte)(0x80 | c & 0x3F);} else { // @6buffer[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)位的值,从输入中读取字符串可以研磨,是根据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序列化的整体结构为length +内容,注意,这里的length不是内容字节的长度,而是String字符的长度。
- 如果是null,则用1个字节表示,其二进制为1000 0000。
- 如果是“”空字符串,则用1个字节表示,其二进制为1000 0001。
- 如果字符长度大于1且小于64,并且字符全是ascii字符(小等于127),则每个字符用一个字节表示,最后一个字节的高位置1,表示字符串字符的结束。【优化点,如果是ascii字符,编码时不需要使用length +内容的方式,或者直接写入内容】
- 如果不满足上述条件,则需要使用length +内容的方式。
- 用一个变长int写入字符的长度,每个字节,高两个分别为编码标记(1:utf8),是否结束标记(1:否; 0:结束) 2)将内容用utf-8编码的字符串序列中,utf8,用变长字节(1-3)个字节表示一个字符(英文,中文)。每一个字节,使用6为,高两位为标志位。【16位】 3字节的存储为【4位】+【6位】+【6位】,根据第一个字节高4位判断得出需要几个字节来存储一个字符。
其反序列化的入口为Input#readString,就是按照上述规则进行解析即可,就不深入探讨了,有兴趣的话,可以自己去指定地方查阅。
3,布尔类型序列化
实现类为DefaultSerializers $ BooleanSerializer,序列化:使用1个字节存储boolean类型,如果为true,则写入1,否则写入0。
4,字节类型序列化
实现类为:DefaultSerializers $ ByteSerializer,序列化:直接将字节写入字节流中即可。
5,char类型序列化
实现类为:DefaultSerializers $ CharSerializer
输出#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写入字节流
6,短类型序列化
实现类为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类型序列化一样,采用大端字节顺序存储。
7,long类型序列化
实现类为:DefaultSerializers $ LongSerializer
输出#writeLong
public int writeLong (long value, boolean optimizePositive) throws KryoException {return writeVarLong(value, optimizePositive);}
序列化:采用变长字节(1-9)位来存储long,其编码规则与int变长类型一致,每个字节的高位表示表示结束,1:表示还需要继续读取下一个字节,0:表示结束。
8,浮点类型序列化
实现类为: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个字节,反而消耗的存储空间大小。
9,DefaultSerializers $ DoubleSerializer
输出#writeDouble序列化:首先将Double遵循IEEE 754编码标准转换为Long,然后才去固定8字节存储。随即,介绍了8种基本类型(boolean,byte,char,short,int,float, long,double)和String类型的序列化与反序列化。
10,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的内容即可。
11,BigDecimal序列化
实现类为:DefaultSerializers $ BigDecimalSerializer
BigDecimal的序列化与BigInteger相同,首先是通过BigDecimal#unscaledValue方法返回对应的BigInteger,然后序列化,在反序列化时通过BigInteger创建对应的BigDecimal即可。
12,类实例序列化
实现类为:DefaultSerializers $ ClassSerializer
public void write (Kryo kryo, Output output, Class object) {kryo.writeClass(output, object); // @1output.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 }}
代码@ 2:完成一次写入后,需要重置Kryo中的临时数据结构,这也就是kryo实例非线程安全的原因,其中一些重要的数据结构会。代码@ 1:首先调用ClassResolver.wreteClass方法。再ClassResolver.writeClass中详细说明。
DefaultClassResolver#writeClass
public Registration writeClass (Output output, Class type) { if (type == null) { // @1if (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) // @3writeName(output, type, registration); else {if (TRACE) trace("kryo", "Write class " + registration.getId() + ": " + className(type));output.writeVarInt(registration.getId() + 2, true); // @4 } return registration;}
代码@ 1:如果类型为null,则存储Kryo.NULL(0),使用变长int来存储,0在变长int中占用1个字节。
代码@ 2:根据类型从kryo获取类注册信息,如果有调用kryo#public注册寄存器(类类型)方法,则返回其注册关系。
代码@ 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); // @1if (classToNameId != null) { // @2int 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++; // @3if (classToNameId == null) classToNameId = new IdentityObjectIntMap(); // @4classToNameId.put(type, nameId); // @5output.writeVarInt(nameId, true); // @6output.writeString(type.getName()); // @7}
代码@ 1:由于是要写入类的全路径名,而最初使用变长int编码写入一个标记,表示是存储的类名,而不是一个ID。其标志位为NAME + 2 =1。存储0表示为空。
代码@ 2:如果classToNameId不为空(IdentityObjectIntMap ),根据类型获取nameId,如果不为空并且从缓存中能获取到nameId,则直接写入nameId,而不是写入类名,这里指在一次序列化过程中,同一个类名例如(cn.uce.test.Test)只写入一次,其他级联(重复)出现时,为此分配一个ID,进行缓存,具体可以从下面的代码中知道其必然。
代码@ 3:首先分配一扩展递增的nameId。
代码@ 4:如果classToNameId为空,则创建一个实例。
代码@ 5:将类型与nameId进行缓存。
代码@ 6:写入nameId。代码@ 7:写入type的全路径名。
注意Kryo#writeClass,一次序列化Class实例后会调用reset方法,最终会清除本次classToNameId,classToNameId并不能做一个全据的缓存的确实是,在不同的JVM虚拟机中,同一个类类型对应的nameId不一定相同,故无法实现共存,只能是作为一个优化,在一次类序列化中,如果存在同一个类型,则第一个写入类全路径名,后面出现的则使用id(int )来存储,节省空间。
为了加深上述理解,我们再来看一下Class实例的反序列化:
DefaultClassResolver#readClass
public Registration readClass (Input input) {int classID = input.readVarInt(true); // @1switch (classID) {case Kryo.NULL: // @2if (TRACE || (DEBUG && kryo.getDepth() == 1)) log("Read", null);return null;case NAME + 2: // Offset for NAME and NULL. // @3return 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.forName生成对应的实例。
kryo序列化Class实例的编码规则:
- 如果为null,用变长int,实际使用1个字节,存储变量0。
- 如果该类通过类注册机制注册到kryo时,则序列化(nameId + 2),用变长int存储。
- 如果该类未通过类注册机制注册到kryo,在一次序列化过程中(包含级联)时,类型第一次出现时,会分配一个nameId,将nameId + type全路径序列化,后续再出现该类型,则只序列化nameId即可。
13,DefaultSerializers $ DateSerializer
java.Util.Date,java.sql.Date等序列化时,只需序列化Date#getTime()返回的long类型,反序列化时根据long类型创建对应的实例即可。long类型的编码使用变长long格式进行序列化。
14,枚举类型Enum序列化
实现类为:DefaultSerializers $ EnumSerializer
static public class EnumSerializer extends Serializer {{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 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()获取枚举类型的常量数组,然后从二进制流中获取下标即可。-
15,EnumSet类型序列化
实现类为:DefaultSerializers $ EnumSetSerializer
static public class EnumSetSerializer extends Serializer {public void write (Kryo kryo, Output output, EnumSet object) {Serializer serializer;if (object.isEmpty()) { // @1EnumSet tmp = EnumSet.complementOf(object); // @2if (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); // @4for (Object element : object) // @5serializer.write(kryo, output, element); }public EnumSet read (Kryo kryo, Input input, Class 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中的枚举类型类实例,并获取枚举类型对应的序列器。
代码@ 4:序列化EnumSet中元素的个数。
代码@ 5:逐一序列化EnumSet中元素(一个个枚举值)。
16,StringBuffer序列化
实现类为DefaultSerializers $ StringBufferSerializer,序列化:与String序列化一致。
17,StringBuilder序列化
实现类为DefaultSerializers $ StringBuilderSerializer,序列化:与String序列化一致。
18,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);elsekryo.writeObject(output, entry.getKey(), keySerializer);} elsekryo.writeClassAndObject(output, entry.getKey());if (valueSerializer != null) {if (valuesCanBeNull)kryo.writeObjectOrNull(output, entry.getValue(), valueSerializer);elsekryo.writeObject(output, entry.getValue(), valueSerializer);} elsekryo.writeClassAndObject(output, entry.getValue());}复制代码
其序列化方法就是遍历Map中的元素,调用Kryo#writeClassAndObject进行序列化,Kryo#writeClassAndObject涉及到Kryo整个序列化流程,将在下节介绍。
本节就认为到这里了,本节详细分析了Kryo对各种数据类型的序列化机制,其再降低序列化大小方面做了如下优化:-
- Kryo序列化的“对象”是数据以及少量元信息,这和JAVA默认的序列化的本质区别,java默认的序列化的目的是语言层次的,将类,对象的所有信息都序列化了,也就是就算是不加载类的定义,也可以根据序列化后的信息动态生成类的所有信息。而Kryo反序列化时,必须能加载类的定义,这样Kryo可以节省大量的字节空间。
- 使用变长int,变长long存储int,long类型,大大节省空间。
- 元数据(字符串类型)使用缓存机制,重复出现的字符串使用int来存储,节省存储空间。
- 字符串类型使用UTF-8存储,但会使用ascii码进一步优化空间。
作者:中间件兴趣圈
链接:https://juejin.im/post/6844904127353339917
来源:掘金
本文详细探讨了Kryo序列化框架为何高效,主要从序列化后的二进制序列大小和序列化速率两方面展开。文章通过对Java常用数据类型的序列化机制的剖析,揭示了Kryo如何通过变长编码、ASCII优化、字符串编码规则等方式减少序列化二进制流的大小。此外,还介绍了Kryo在反序列化、布尔类型、字符串、long类型以及其他数据类型的序列化实现,强调了类实例序列化中的类注册机制以及EnumSet、TreeMap等特殊类型的序列化策略。
320

被折叠的 条评论
为什么被折叠?



